Files
mtgodot-poc/project/motion_splash.gd
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

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}