fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# WeaponTrace —— 40250 GameLib/WeaponTrace.cpp 的 Godot 适配层。
|
||||
#
|
||||
# 40250 的刀光不是一次性 MSE 特效:AttachWeapon 为可追踪武器创建对象,
|
||||
# ActorInstance 每帧采样武器实例的刀根/刀尖世界坐标,RenderTrace 再把
|
||||
# short/long 两条时间轨迹用 cubic spline 拼成 triangle strip。这里保留同一
|
||||
# 状态边界和参数;渲染坐标在世界空间采样,在当前 PlayerView 局部渲染,
|
||||
# 因此角色移动时已经生成的尾迹不会被父节点位移二次带走。
|
||||
extends Node3D
|
||||
|
||||
const DEFAULT_LIFETIME := 0.18
|
||||
const DEFAULT_SAMPLING_TIME := 0.003
|
||||
const TRACE_COLOR := Color(0.3, 0.8, 1.0, 1.0)
|
||||
|
||||
var lifetime := DEFAULT_LIFETIME
|
||||
var sampling_time := DEFAULT_SAMPLING_TIME
|
||||
var reach_scale := 1.0 # CActorInstance::__GetReachScale()
|
||||
var use_texture := false
|
||||
var texture_path := ""
|
||||
|
||||
var _weapon: Node3D
|
||||
var _playing := false
|
||||
var _has_geometry := false
|
||||
var _tip_local := Vector3.ZERO
|
||||
var _short_points: Array = [] # newest first:刀根
|
||||
var _long_points: Array = [] # newest first:刀尖
|
||||
var _vertex_count := 0
|
||||
|
||||
var _mesh := ImmediateMesh.new()
|
||||
var _mesh_instance := MeshInstance3D.new()
|
||||
var _material := StandardMaterial3D.new()
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_render_node()
|
||||
|
||||
func _ensure_render_node() -> void:
|
||||
if _mesh_instance.get_parent() == null:
|
||||
_mesh_instance.name = "WeaponTraceMesh"
|
||||
_mesh_instance.mesh = _mesh
|
||||
_mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(_mesh_instance)
|
||||
_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_material.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
||||
_material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
_material.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
_material.vertex_color_use_as_albedo = true
|
||||
_mesh_instance.material_override = _material
|
||||
_mesh_instance.visible = false
|
||||
|
||||
func attach_to_weapon(weapon_node: Node3D) -> bool:
|
||||
if _weapon == weapon_node and is_instance_valid(_weapon):
|
||||
return _has_geometry
|
||||
turn_off()
|
||||
clear()
|
||||
_weapon = weapon_node
|
||||
_has_geometry = false
|
||||
_tip_local = Vector3.ZERO
|
||||
if not is_instance_valid(_weapon):
|
||||
return false
|
||||
_has_geometry = _resolve_tip_local()
|
||||
return _has_geometry
|
||||
|
||||
func detach_weapon() -> void:
|
||||
turn_off()
|
||||
clear()
|
||||
_weapon = null
|
||||
_has_geometry = false
|
||||
_tip_local = Vector3.ZERO
|
||||
|
||||
func turn_on() -> void:
|
||||
# 40250 TurnOn only changes the playing flag; the existing tail remains and
|
||||
# naturally fades, so combo segments do not pop the trace between swings.
|
||||
_playing = true
|
||||
|
||||
func turn_off() -> void:
|
||||
# 40250 TurnOff deliberately does not Clear(): old samples remain visible
|
||||
# until lifetime expiry. Equipment replacement uses detach_weapon(), which
|
||||
# is the DestroyWeaponTrace/Clear path instead.
|
||||
_playing = false
|
||||
|
||||
func set_lifetime(value: float) -> void:
|
||||
lifetime = maxf(value, 0.001)
|
||||
_trim_expired()
|
||||
|
||||
func set_sampling_time(value: float) -> void:
|
||||
sampling_time = maxf(value, 0.0001)
|
||||
|
||||
func set_reach_scale(value: float) -> void:
|
||||
# 40250 applies this scale when extending the trace from the bone matrix;
|
||||
# recompute the cached local endpoint so an actor-scale change takes effect
|
||||
# before the next sample.
|
||||
reach_scale = maxf(value, 0.0)
|
||||
if is_instance_valid(_weapon):
|
||||
_has_geometry = _resolve_tip_local()
|
||||
|
||||
func set_texture(path: String) -> void:
|
||||
texture_path = path
|
||||
use_texture = path != ""
|
||||
_material.albedo_texture = _load_texture(path) if use_texture else null
|
||||
|
||||
func use_alpha() -> void:
|
||||
use_texture = false
|
||||
_material.albedo_texture = null
|
||||
|
||||
func use_texture_mode() -> void:
|
||||
use_texture = true
|
||||
_material.albedo_texture = _load_texture(texture_path) if texture_path != "" else null
|
||||
|
||||
func is_playing() -> bool:
|
||||
return _playing
|
||||
|
||||
func debug_state() -> Dictionary:
|
||||
return {
|
||||
"playing": _playing,
|
||||
"has_geometry": _has_geometry,
|
||||
"short_points": _short_points.size(),
|
||||
"long_points": _long_points.size(),
|
||||
"vertex_count": _vertex_count,
|
||||
"lifetime": lifetime,
|
||||
"sampling_time": sampling_time,
|
||||
"reach_scale": reach_scale,
|
||||
"tip_local": _tip_local,
|
||||
"tip_length": _tip_local.length(),
|
||||
"use_texture": use_texture,
|
||||
}
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_ensure_render_node()
|
||||
var elapsed := maxf(delta, 0.0)
|
||||
_age_points(elapsed)
|
||||
if _playing and _has_geometry and is_instance_valid(_weapon):
|
||||
_sample_weapon()
|
||||
_render_trace()
|
||||
|
||||
func _sample_weapon() -> void:
|
||||
var root_world := _weapon.global_transform.origin
|
||||
var tip_world := _weapon.global_transform * (_tip_local)
|
||||
_short_points.push_front({"age": 0.0, "pos": root_world})
|
||||
_long_points.push_front({"age": 0.0, "pos": tip_world})
|
||||
_trim_expired()
|
||||
|
||||
func _age_points(delta: float) -> void:
|
||||
for sample in _short_points:
|
||||
sample["age"] = float(sample.get("age", 0.0)) + delta
|
||||
for sample in _long_points:
|
||||
sample["age"] = float(sample.get("age", 0.0)) + delta
|
||||
_trim_expired()
|
||||
|
||||
func _trim_expired() -> void:
|
||||
while not _short_points.is_empty() and float(_short_points.back().get("age", 0.0)) > lifetime:
|
||||
_short_points.pop_back()
|
||||
while not _long_points.is_empty() and float(_long_points.back().get("age", 0.0)) > lifetime:
|
||||
_long_points.pop_back()
|
||||
|
||||
func clear() -> void:
|
||||
_short_points.clear()
|
||||
_long_points.clear()
|
||||
_vertex_count = 0
|
||||
if is_instance_valid(_mesh):
|
||||
_mesh.clear_surfaces()
|
||||
if is_instance_valid(_mesh_instance):
|
||||
_mesh_instance.visible = false
|
||||
|
||||
func _resolve_tip_local() -> bool:
|
||||
# CWeaponTrace::SetWeaponInstance computes m_fLength from the maximum
|
||||
# distance between the weapon bone origin and GetBoundBox() corners. It does
|
||||
# not select that corner as the direction of the blade. Update() then moves
|
||||
# along the composite bone's local Z axis by m_fLength * reach_scale.
|
||||
var inverse := _weapon.global_transform.affine_inverse()
|
||||
var farthest := 0.0
|
||||
var meshes: Array[MeshInstance3D] = []
|
||||
if _weapon is MeshInstance3D:
|
||||
meshes.append(_weapon as MeshInstance3D)
|
||||
for child in _weapon.find_children("*", "MeshInstance3D", true, false):
|
||||
var mi := child as MeshInstance3D
|
||||
if mi != null and not meshes.has(mi):
|
||||
meshes.append(mi)
|
||||
for mi in meshes:
|
||||
if mi.mesh == null:
|
||||
continue
|
||||
var box := mi.get_aabb()
|
||||
for corner in 8:
|
||||
var local_point := box.position + Vector3(
|
||||
box.size.x if (corner & 1) != 0 else 0.0,
|
||||
box.size.y if (corner & 2) != 0 else 0.0,
|
||||
box.size.z if (corner & 4) != 0 else 0.0)
|
||||
var world_point: Vector3 = mi.global_transform * local_point
|
||||
var weapon_local: Vector3 = inverse * world_point
|
||||
var distance := weapon_local.length_squared()
|
||||
if distance > farthest:
|
||||
farthest = distance
|
||||
if farthest <= 0.000001 or is_zero_approx(reach_scale):
|
||||
return false
|
||||
_tip_local = Vector3(0.0, 0.0, sqrt(farthest) * reach_scale)
|
||||
return _tip_local.length_squared() > 0.000001
|
||||
|
||||
func _render_trace() -> void:
|
||||
if _short_points.size() <= 1 or _long_points.size() <= 1:
|
||||
_vertex_count = 0
|
||||
_mesh.clear_surfaces()
|
||||
_mesh_instance.visible = false
|
||||
return
|
||||
var vertices := _build_vertices()
|
||||
_vertex_count = vertices.size()
|
||||
_mesh.clear_surfaces()
|
||||
if vertices.size() < 4:
|
||||
_mesh_instance.visible = false
|
||||
return
|
||||
_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP, _material)
|
||||
for vertex in vertices:
|
||||
_mesh.surface_set_color(Color(TRACE_COLOR.r, TRACE_COLOR.g, TRACE_COLOR.b,
|
||||
float(vertex.get("alpha", 0.0))))
|
||||
_mesh.surface_set_uv(vertex.get("uv", Vector2.ZERO))
|
||||
_mesh.surface_add_vertex(to_local(vertex.get("pos", Vector3.ZERO)))
|
||||
_mesh.surface_end()
|
||||
_mesh_instance.visible = true
|
||||
|
||||
func _build_vertices() -> Array:
|
||||
var length := minf(lifetime, float(_long_points.back().get("age", 0.0)))
|
||||
var out: Array = []
|
||||
# Reference BuildVertex renders the long track first (UV y=0), then the
|
||||
# short track (UV y=1), interleaving the pair into a triangle strip.
|
||||
var long_vertices := _build_spline(_long_points, true, length)
|
||||
var short_vertices := _build_spline(_short_points, false, length)
|
||||
var count := mini(long_vertices.size(), short_vertices.size())
|
||||
for i in count:
|
||||
out.append(long_vertices[i])
|
||||
out.append(short_vertices[i])
|
||||
return out
|
||||
|
||||
func _build_spline(input: Array, is_long: bool, length: float) -> Array:
|
||||
var out: Array = []
|
||||
var n := input.size() - 1
|
||||
if n <= 0:
|
||||
return out
|
||||
var h: Array = []
|
||||
var r: Array = []
|
||||
h.resize(n + 1)
|
||||
r.resize(n + 1)
|
||||
for i in n:
|
||||
var dt := float(input[i + 1].get("age", 0.0)) - float(input[i].get("age", 0.0))
|
||||
if dt <= 0.000001:
|
||||
return out
|
||||
h[i] = dt
|
||||
var p0: Vector3 = input[i].get("pos", Vector3.ZERO)
|
||||
var p1: Vector3 = input[i + 1].get("pos", Vector3.ZERO)
|
||||
r[i] = (p1 - p0) * (3.0 / dt)
|
||||
r[n] = Vector3.ZERO
|
||||
for i in range(n, 0, -1):
|
||||
r[i] = r[i] + r[i - 1]
|
||||
var stack: Array = []
|
||||
var rate := 0.5
|
||||
r[0] = r[0] * 0.5
|
||||
stack.append(rate)
|
||||
for i in range(1, n):
|
||||
r[i] = r[i] - r[i - 1]
|
||||
rate = 1.0 / (4.0 - rate)
|
||||
r[i] = r[i] * rate
|
||||
stack.append(rate)
|
||||
r[n] = r[n] - r[n - 1]
|
||||
rate = 1.0 / (2.0 - rate)
|
||||
r[n] = r[n] * rate
|
||||
for i in range(n - 1, -1, -1):
|
||||
r[i] = r[i] - float(stack.pop_back()) * r[i + 1]
|
||||
|
||||
var base := 0
|
||||
var timebase := 0.0
|
||||
var timenext := float(h[0])
|
||||
var delta := maxf(sampling_time, 0.0001)
|
||||
var a: Vector3 = input[0].get("pos", Vector3.ZERO)
|
||||
var b: Vector3 = r[0]
|
||||
var first_delta: Vector3 = input[1].get("pos", Vector3.ZERO) - a
|
||||
var c: Vector3 = (3.0 * first_delta - r[1] * float(h[0]) - 2.0 * float(h[0]) * r[0]) \
|
||||
/ (float(h[0]) * float(h[0]))
|
||||
var d: Vector3 = (-2.0 * first_delta + (r[1] + r[0]) * float(h[0])) \
|
||||
/ (float(h[0]) * float(h[0]) * float(h[0]))
|
||||
var t := 0.0
|
||||
var life := maxf(lifetime, 0.001)
|
||||
while t <= length + 0.000001:
|
||||
while t > timenext and base < n:
|
||||
timebase = timenext
|
||||
base += 1
|
||||
if base >= n:
|
||||
break
|
||||
var seg_delta: Vector3 = input[base + 1].get("pos", Vector3.ZERO) - input[base].get("pos", Vector3.ZERO)
|
||||
a = input[base].get("pos", Vector3.ZERO)
|
||||
b = r[base]
|
||||
c = (3.0 * seg_delta - r[base + 1] * float(h[base]) -
|
||||
2.0 * float(h[base]) * r[base]) / (float(h[base]) * float(h[base]))
|
||||
d = (-2.0 * seg_delta + (r[base + 1] + r[base]) * float(h[base])) / \
|
||||
(float(h[base]) * float(h[base]) * float(h[base]))
|
||||
timenext += float(h[base])
|
||||
if base >= n:
|
||||
break
|
||||
var cc := t - timebase
|
||||
var position: Vector3 = a + cc * (b + cc * (c + cc * d))
|
||||
var ttt := clampf((t + float(input[0].get("age", 0.0))) / life, 0.0, 1.0)
|
||||
var alpha := clampf((1.0 - ttt) * (1.0 - ttt) / 2.5 - 0.1, 0.0, 1.0) if is_long else 0.0
|
||||
out.append({"pos": position, "uv": Vector2(t / life, 0.0 if is_long else 1.0), "alpha": alpha})
|
||||
t += delta
|
||||
return out
|
||||
|
||||
func _load_texture(path: String) -> Texture2D:
|
||||
if path == "":
|
||||
return null
|
||||
if ResourceLoader.exists(path):
|
||||
return ResourceLoader.load(path) as Texture2D
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
var image := Image.load_from_file(path)
|
||||
return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
|
||||
Reference in New Issue
Block a user