- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
312 lines
10 KiB
GDScript
312 lines
10 KiB
GDScript
# 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
|