# fly_object.gd —— 1:1 复刻 GameLib 的飞行物实体系统(CFlyingManager / CFlyingInstance / # CFlyingData),把原客户端「.msa FLY 帧 → OnShoot → CG_SHOOT」之后、由服务端 GC_CREATE_FLY # 驱动的那颗真实弹道 + 命中判定搬到 POC。net_world.gd 收到 client.fly_cue 后调 # FlyManager.spawn(),每帧 step() 推进运动学并做「线段到点」的爆炸判定,命中 / 超程 / 触地 # 时回调 handler(对齐 IFlyEventHandler)。§3.6 # # 参考: # REF/GameLib/FlyingObjectManager.cpp CFlyingManager::CreateFlyingInstanceFlyTarget / # Update()(遍历实例,Update() 返 false 即 Delete + erase)/ CreateIndexedFly(NORMAL / # FIRE_CRACKER / AUTO_FIRE 三种服务端索引飞行) # REF/GameLib/FlyingInstance.cpp CFlyingInstance::Create → __SetDataPointer(m_v3Position=起点、 # m_bAlive、m_fStartTime、由 m_fRollAngle-90 yaw + m_fConeAngle roll 建四元数、可选 # m_bSpreading 随机扩散、m_v3Velocity=m_v3LocalVelocity=(0,-m_fInitVel,0)、m_v3Accel= # pData->m_v3Accel、m_fRemainRange=m_fRange、m_iPierceCount)+ __SetTargetDirection # (v3TargetPos=target.GetFlyTargetPosition();m_bMaintainParallel 时 z+=50;方向= # normalize(target-pos),把局部速度 / 加速度旋到该方向) # REF/GameLib/FlyingInstance.cpp CFlyingInstance::Update():homing → AdjustDirectionForHoming; # v3LastPosition=m_v3Position;m_v3Velocity+=m_v3Accel*dt;m_v3Velocity.z+=m_fGravity*dt; # v3Movement=m_v3Velocity*dt;_fMoveDistance=|v3Movement|;m_fRemainRange-=_fMoveDistance; # m_v3Position+=v3Movement;m_fRemainRange<0 → OnExplodingOutOfRange + __Explode(false); # 对象目标且未命中 && square_distance_between_linesegment_and_point(pos,last,targetPos) < # m_fBombRange² → m_bTargetHitted=TRUE;m_canAttack 时 pVictim->OnShootDamage(); # handler->OnExplodingAtTarget(skill);m_iPierceCount>0 → --、__Bomb();否则 __Explode(); # 位置目标同样线段判定 → __Explode();m_bHitOnBackground → GetTerrainHeight(x,-y)>z → 触地 # REF/GameLib/FlyingInstance.cpp __Explode(bBomb=true):!m_bAlive return;m_bAlive=false; # bBomb → __Bomb()(在 m_v3Position 生成 m_dwBombEffectID 特效) # REF/EterLib/GrpMath.h:80 square_distance_between_linesegment_and_point(点到线段的平方距离, # d<=0 取端点 p1、d>=l 取端点 p2、否则叉积模方 / l) # REF/GameLib/FlyingData.cpp CFlyingData::__Initialize()(本文件 FlyData 默认值逐项对齐) # REF/GameLib/FlyHandler.h IFlyEventHandler:OnExplodingOutOfRange / OnExplodingAtBackground / # 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 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 脚本字段。原版文件使用 pixel==cm;加载器在边界处 # 统一换算为 Godot 米制,因此 init_vel / range / bomb_range / acceleration 等 # 长度量都按 0.01 转换,角度和时间保持原单位。 class FlyData extends RefCounted: 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) var is_homing := false # m_bIsHoming 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 }。 class FlyInstance extends RefCounted: var id := 0 var data: FlyData var pos := Vector3.ZERO var last_pos := Vector3.ZERO var vel := Vector3.ZERO var accel := Vector3.ZERO var remain_range := 0.0 var pierce := 0 var alive := false var can_attack := false 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): _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 can_attack = attack pos = start_world last_pos = start_world 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 = _node_fly_target_position(target as Node3D) elif target is Vector3: _is_object = false _target_pos = target elif target is Dictionary: target_vid = int(target.get("vid", 0)) var tp: Variant = target.get("pos", null) if tp is Vector3: _target_pos = tp var tn: Variant = target.get("node", null) if tn is Node3D: _is_object = true _target_obj = tn _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) if dir.length() < 0.0001: dir = Vector3(0, 0, -1) dir = dir.normalized() vel = dir * d.init_vel accel = d.accel if accel.length_squared() > 0.0: # 局部帧 -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: var tdir := (target_position() - pos) if tdir.length() < 0.0001: return tdir = tdir.normalized() var vdir := vel.normalized() var ang := vdir.angle_to(tdir) if ang < 0.0001: return var maxr := deg_to_rad(data.homing_max_angle) if data.homing_max_angle < 180.0 else ang var t := clampf(maxr / ang, 0.0, 1.0) var speed := vel.length() vel = vdir.slerp(tdir, t) * speed # CFlyingInstance::Update() —— 返 false 表示该销毁(超程 / 命中且不穿透 / 触地)。 func update(dt: float) -> bool: if not alive: return false _elapsed += dt if data.is_homing and _elapsed > data.homing_start_time and _is_object: _adjust_homing() last_pos = pos vel += accel * dt vel.y += data.gravity * dt var movement := vel * dt var move_dist := movement.length() remain_range -= move_dist pos += movement _update_attachments(dt) if remain_range < 0.0: _emit("out_of_range") _explode(false) return false var br2 := data.bomb_range * data.bomb_range if _is_object: if not target_hitted: if _seg_point_sq_dist(pos, last_pos, target_position()) < br2: target_hitted = true if can_attack: _emit("shoot_damage") _emit("at_target") if pierce > 0: pierce -= 1 _emit("bomb") else: _explode() return false return true else: if _seg_point_sq_dist(pos, last_pos, target_position()) < br2: _emit("at_position") _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)) if gh > pos.y: _emit("at_background") _explode() return false # 40250 CFlyingInstance::Update FCheckBackgroundDuringFlying: # 空间 3D 建筑与静态障碍物碰撞(碰撞层 2: StaticBody3D) if _world is Node3D and _world.is_inside_tree(): var w3d: World3D = _world.get_world_3d() if w3d and w3d.direct_space_state: var q := PhysicsRayQueryParameters3D.create(last_pos, pos, 2) var hit := w3d.direct_space_state.intersect_ray(q) if not hit.is_empty(): pos = hit.position _emit("at_background") _explode() return false return true func _explode(bomb := true) -> void: if not alive: return alive = false if bomb: _emit("bomb") 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, 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: var v := p2 - p1 var l := v.length_squared() var w := x - p1 var d := w.dot(v) if d <= 0.0: return w.length_squared() if d >= l: return (x - p2).length_squared() return w.cross(v).length_squared() / l 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, 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, 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) 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() mat.emission_enabled = true mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA if skill_index == FLY_EXP: # 40250 官方经验聚能光球: ga_piece_yellow_small2.msf sm.radius = 0.12 sm.height = 0.24 mat.albedo_color = Color(1.0, 0.9, 0.2, 0.95) mat.emission = Color(1.0, 0.8, 0.1) * 3.0 elif skill_index in [FLY_HP_SMALL, FLY_HP_MEDIUM, FLY_HP_BIG]: # 40250 官方生命吸取光球: ga_piece_red_small.msf sm.radius = 0.12 sm.height = 0.24 mat.albedo_color = Color(1.0, 0.25, 0.25, 0.95) mat.emission = Color(1.0, 0.1, 0.1) * 3.0 elif skill_index in [FLY_SP_SMALL, FLY_SP_MEDIUM, FLY_SP_BIG]: # 40250 官方法力吸取光球: ga_piece_blue_small.msf sm.radius = 0.12 sm.height = 0.24 mat.albedo_color = Color(0.25, 0.6, 1.0, 0.95) mat.emission = Color(0.1, 0.5, 1.0) * 3.0 else: sm.radius = 0.08 sm.height = 0.16 mat.albedo_color = Color(1.0, 0.9, 0.4) mat.emission = Color(0.9, 0.7, 0.2) m.mesh = sm m.material_override = mat parent.add_child(m) if m.is_inside_tree(): m.global_position = inst.pos else: m.position = inst.pos _visuals[inst] = m return inst 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] var keep := inst.update(dt) var vis: MeshInstance3D = _visuals.get(inst, null) if is_instance_valid(vis): if vis.is_inside_tree(): vis.global_position = inst.pos else: vis.position = inst.pos if not keep: _release_instance(inst) if is_instance_valid(vis): vis.queue_free() _visuals.erase(inst) _instances.remove_at(i) else: i += 1 func _process(dt: float) -> void: if not manual_step: step(dt) func _on_instance_event(inst: FlyInstance, ev: String, wp: Vector3, vid: int) -> void: match ev: "shoot_damage": shoot_damage.emit(vid) "out_of_range", "at_target", "at_position", "at_background", "at_another": exploded.emit(wp, ev) if ev == "at_target" and inst.skill_index == FLY_EXP: exp_absorbed.emit(wp) # 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: 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() var sm := SphereMesh.new() sm.radius = 0.06 sm.height = 0.12 f.mesh = sm var mat := StandardMaterial3D.new() mat.emission_enabled = true mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA var final_scale := Vector3.ONE * 6.0 if skill_index == FLY_EXP: mat.albedo_color = Color(1.0, 0.9, 0.2, 0.95) mat.emission = Color(1.0, 0.85, 0.2) * 2.5 final_scale = Vector3.ONE * 8.0 elif skill_index in [FLY_HP_SMALL, FLY_HP_MEDIUM, FLY_HP_BIG]: mat.albedo_color = Color(1.0, 0.25, 0.25, 0.95) mat.emission = Color(1.0, 0.1, 0.1) * 2.5 final_scale = Vector3.ONE * 8.0 elif skill_index in [FLY_SP_SMALL, FLY_SP_MEDIUM, FLY_SP_BIG]: mat.albedo_color = Color(0.25, 0.6, 1.0, 0.95) mat.emission = Color(0.1, 0.5, 1.0) * 2.5 final_scale = Vector3.ONE * 8.0 else: mat.albedo_color = Color(1.0, 0.75, 0.3, 0.9) mat.emission = Color(1.0, 0.6, 0.2) f.material_override = mat parent.add_child(f) _flash_nodes.append(f) if f.is_inside_tree(): f.global_position = wp else: f.position = wp var tw := create_tween() 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(func() -> void: _flash_nodes.erase(f) if is_instance_valid(f): f.queue_free() )