Files

315 lines
12 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/ CreateIndexedFlyNORMAL /
# FIRE_CRACKER / AUTO_FIRE 三种服务端索引飞行)
# REF/GameLib/FlyingInstance.cpp CFlyingInstance::Create → __SetDataPointerm_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_v3Positionm_v3Velocity+=m_v3Accel*dtm_v3Velocity.z+=m_fGravity*dt
# v3Movement=m_v3Velocity*dt_fMoveDistance=|v3Movement|m_fRemainRange-=_fMoveDistance
# m_v3Position+=v3Movementm_fRemainRange<0 → OnExplodingOutOfRange + __Explode(false)
# 对象目标且未命中 && square_distance_between_linesegment_and_point(pos,last,targetPos) <
# m_fBombRange² → m_bTargetHitted=TRUEm_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 returnm_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 IFlyEventHandlerOnExplodingOutOfRange / OnExplodingAtBackground /
# OnExplodingAtAnotherTarget(skill,vid) / OnExplodingAtTarget(skill)
extends Node
# CFlyingManager::EIndexFlyTypeREF/GameLib/FlyingObjectManager.h
const INDEX_FLY_TYPE_NORMAL := 0
const INDEX_FLY_TYPE_FIRE_CRACKER := 1
const INDEX_FLY_TYPE_AUTO_FIRE := 2
# CFlyingData —— .fly 脚本字段。POC 无 .fly 加载器,默认值逐项对齐 CFlyingData::__Initialize()
# (长度单位:原版是 pixel==cm,这里换算成米后传进来,故 init_vel / range / bomb_range 都按米)。
class FlyData extends RefCounted:
var init_vel := 2.0 # m_fInitVel=200cm/s)→ 2 m/s(占位;net_world 用弓箭值覆盖)
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 flat_range := 5.0 # m_fRange=500cm)→ 5 m
var bomb_range := 0.1 # m_fBombRange=10cm)→ 0.1 m
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
# 单颗飞行物 —— 对齐 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 _target_obj: Node3D
var _target_pos := Vector3.ZERO
var _is_object := false
var _elapsed := 0.0
var _world: Object # 可空:有 sample_height 时做触地
# handler.call(event: String, world_pos: Vector3, vid: int) —— 对齐 IFlyEventHandler
var handler := Callable()
func target_position() -> Vector3:
if _is_object and is_instance_valid(_target_obj):
_target_pos = _target_obj.global_position
var tp := _target_pos
if data and data.maintain_parallel:
tp.y += 0.5
return tp
# 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
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
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 = (tn as Node3D).global_position
# __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
# 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
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
if data.hit_on_background and _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
return true
func _explode(bomb := true) -> void:
if not alive:
return
alive = false
if bomb:
_emit("bomb")
func _emit(ev: String) -> void:
if handler.is_valid():
handler.call(ev, pos, target_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 manual_step := false # true 时 _process 不自动推进(headless 测试用 step() 手动驱动)
var _instances: Array = [] # Array[FlyInstance]
var _visuals := {} # FlyInstance -> MeshInstance3D
var _id_counter := 1
signal shoot_damage(target_vid: int)
signal exploded(world_pos: Vector3, cause: String)
func setup(mount: Node3D, w: Object = null) -> void:
parent = mount
world = w
# CFlyingManager::CreateFlyingInstanceFlyTarget —— 建一颗飞行物并入表。
func spawn(start_world: Vector3, target, can_attack: bool, data: FlyData = null,
skill_index := 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._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)
_instances.append(inst)
if parent != null:
var m := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.08
sm.height = 0.16
m.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.9, 0.4)
mat.emission_enabled = true
mat.emission = Color(0.9, 0.7, 0.2)
m.material_override = mat
parent.add_child(m)
m.global_position = inst.pos
_visuals[inst] = m
return inst
func active_count() -> int:
return _instances.size()
# CFlyingManager::Update() —— 遍历实例,Update() 返 false 即删除。
func step(dt: float) -> void:
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):
vis.global_position = inst.pos
if not keep:
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 parent != null and ev != "out_of_range":
_spawn_flash(wp)
"bomb":
if parent != null:
_spawn_flash(wp)
func _spawn_flash(wp: Vector3) -> void:
var f := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = 0.05
sm.height = 0.1
f.mesh = sm
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.75, 0.3, 0.9)
mat.emission_enabled = true
mat.emission = Color(1.0, 0.6, 0.2)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
f.material_override = mat
parent.add_child(f)
f.global_position = wp
var tw := create_tween()
tw.set_parallel(true)
tw.tween_property(f, "scale", Vector3.ONE * 6.0, 0.25)
tw.tween_property(mat, "albedo_color:a", 0.0, 0.25)
tw.chain().tween_callback(f.queue_free)