- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
108 lines
4.5 KiB
GDScript
108 lines
4.5 KiB
GDScript
# MotionSplash —— 40250 CActorInstance::SSplashArea 的无渲染状态与几何。
|
|
#
|
|
# 参考链路:
|
|
# ActorInstanceMotionEvent.cpp::ProcessMotionEventSpecialAttacking
|
|
# -> ActorInstanceCollisionDetection.cpp::__SplashAttackProcess
|
|
# -> CheckCollisionDetection / __ProcessDataAttackSuccess
|
|
#
|
|
# 坐标仍使用客户端的 Metin2 cm 约定做碰撞(X、Y 为地面平面,Z 为高度);
|
|
# 对外只在 begin/try_hit 边界转换为 Godot 米,避免把攻击球误当成普通特效。
|
|
class_name MotionSplash
|
|
|
|
const CM := 100.0
|
|
const ATTACK_TYPE_SNIPE := 1 # NRaceData::ATTACK_TYPE_SNIPE
|
|
|
|
# ActorInstanceMotionEvent.cpp 的原式:
|
|
# fRadian = D3DXToRadian(270.0f + 360.0f - GetRotation())
|
|
# x = actor.x + src.x*sin(fRadian) + src.y*cos(fRadian)
|
|
# y = actor.y + src.x*cos(fRadian) - src.y*sin(fRadian)
|
|
# z = actor.z + src.z
|
|
static func _motion_event_sphere(owner_world: Vector3, yaw: float,
|
|
source: Vector3, radius: float) -> Dictionary:
|
|
var owner_cm := Vector3(owner_world.x * CM, -owner_world.z * CM, owner_world.y * CM)
|
|
var rad := deg_to_rad(270.0 + 360.0 - rad_to_deg(yaw))
|
|
var center_cm := Vector3(
|
|
owner_cm.x + source.x * sin(rad) + source.y * cos(rad),
|
|
owner_cm.y + source.x * cos(rad) - source.y * sin(rad),
|
|
owner_cm.z + source.z)
|
|
# M2 (x, y, z) -> Godot (x, y, z): world z is -M2 y.
|
|
var center_world := Vector3(center_cm.x / CM, center_cm.z / CM, -center_cm.y / CM)
|
|
return {"radius": radius, "pos": center_cm, "world_pos": center_world,
|
|
"last": center_cm}
|
|
|
|
static func begin(owner_vid: int, skill: int, owner_world: Vector3, yaw: float,
|
|
event: Dictionary, now: float) -> Dictionary:
|
|
var spheres: Array = []
|
|
var raw: Variant = event.get("collision_spheres", [])
|
|
if raw is Array:
|
|
for item in raw:
|
|
if not item is Dictionary:
|
|
continue
|
|
var src_variant: Variant = item.get("pos", Vector3.ZERO)
|
|
var source: Vector3 = src_variant if src_variant is Vector3 else Vector3.ZERO
|
|
spheres.append(_motion_event_sphere(owner_world, yaw, source,
|
|
float(item.get("radius", 0.0))))
|
|
return {
|
|
"owner_vid": owner_vid,
|
|
"skill": skill,
|
|
"enable_hit_process": bool(event.get("enable_hit_process", true)),
|
|
"attack_type": int(event.get("attack_type", 0)),
|
|
"hitting_type": int(event.get("hitting_type", 0)),
|
|
"stiffen_time": float(event.get("stiffen_time", 0.0)),
|
|
"invisible_time": float(event.get("invisible_time", 0.0)),
|
|
"external_force": float(event.get("external_force", 0.0)),
|
|
"hit_limit_count": int(event.get("hit_limit_count", 0)),
|
|
"started_at": now,
|
|
"expires_at": now + float(event.get("duration_time", 0.0)),
|
|
"spheres": spheres,
|
|
"hitted": {},
|
|
}
|
|
|
|
static func is_active(area: Dictionary, now: float) -> bool:
|
|
return not area.is_empty() and now < float(area.get("expires_at", 0.0))
|
|
|
|
static func _sphere_position(item: Dictionary, key: String) -> Vector3:
|
|
var v: Variant = item.get(key, Vector3.ZERO)
|
|
return v if v is Vector3 else Vector3.ZERO
|
|
|
|
# CheckCollisionDetection:攻击球与受击方 m_DefendingPointInstanceList 的每个球
|
|
# 做完整三维 DynamicSphere 相交;SpecialAttack 的参考实现不是 Z 圆柱扫描。
|
|
static func try_hit(area: Dictionary, victim_vid: int, defending_spheres: Array,
|
|
now: float) -> Dictionary:
|
|
if not is_active(area, now) or victim_vid <= 0 or defending_spheres.is_empty():
|
|
return {"hit": false}
|
|
var hitted: Dictionary = area.get("hitted", {})
|
|
if hitted.has(victim_vid):
|
|
return {"hit": false}
|
|
var hit_pos := Vector3.ZERO
|
|
var collided := false
|
|
for attack_item in area.get("spheres", []):
|
|
if not attack_item is Dictionary:
|
|
continue
|
|
var attack_pos := _sphere_position(attack_item, "pos")
|
|
var attack_radius := float(attack_item.get("radius", 0.0))
|
|
for victim_item in defending_spheres:
|
|
if not victim_item is Dictionary:
|
|
continue
|
|
var victim_pos := _sphere_position(victim_item, "pos")
|
|
var victim_radius := float(victim_item.get("radius", 0.0))
|
|
var limit := attack_radius + victim_radius
|
|
if attack_pos.distance_squared_to(victim_pos) <= limit * limit:
|
|
hit_pos = (attack_pos + victim_pos) * 0.5
|
|
collided = true
|
|
break
|
|
if collided:
|
|
break
|
|
if not collided:
|
|
return {"hit": false}
|
|
# __SplashAttackProcess inserts before checking the per-event hit cap. Keep
|
|
# that detail: later frames must not retry a victim rejected by the cap.
|
|
hitted[victim_vid] = now
|
|
area["hitted"] = hitted
|
|
var max_count := int(area.get("hit_limit_count", 0))
|
|
if max_count == 0:
|
|
max_count = 16
|
|
if hitted.size() > max_count:
|
|
return {"hit": false, "hit_position": hit_pos, "overflow": true}
|
|
return {"hit": true, "hit_position": hit_pos, "overflow": false}
|