Outline: the inverted-hull width was tuned when the look was heavier and now reads as a thick marker stroke. Cut every call site to roughly a third (characters 0.012 -> 0.004, level geometry 0.015 -> 0.005, Neon Alley's size-scaled clamp 0.02..0.05 -> 0.007..0.018) plus the shader default, so the ink line stays readable without fattening every silhouette. Third-person weapon on HumanoidModel: the gun was parented to root_pivot at a fixed (-0.15, 1.0, 0.4) — floating 40 cm off the chest while both arms posed at it from a distance. Give the rig a real right-hand node at the end of the forearm and hang the weapon there, so it travels with the arm. Its aim is re-derived from the body each frame (hand_r_pivot's global basis is copied from root_pivot), otherwise the barrel would swing around with the arm animation. The hold pose is retuned to match: right elbow tucked at the ribs with a level forearm, left arm crossing to the handguard. _seat_weapon_in_hand() slides each weapon along its own barrel axis so a plausible grip lands in the fist regardless of where the artist left the model origin. It measures with MeshInstance3D.get_aabb(), not mesh.get_aabb() — the FBX gun parts are skinned, so the Mesh resource still carries bind-pose bounds tens of metres across. Co-Authored-By: Claude Opus 4.8 <[email protected]>
58 lines
1.7 KiB
GDScript
58 lines
1.7 KiB
GDScript
extends Node3D
|
|
class_name HitscanTracer
|
|
|
|
var speed: float = 300.0 # Extremely fast
|
|
var target_position: Vector3
|
|
var distance_traveled: float = 0.0
|
|
var max_distance: float = 0.0
|
|
|
|
|
|
## Spawn a cel-styled tracer bolt: unshaded hot core with an ink outline,
|
|
## flying from origin to target. Shared by every hitscan weapon and the
|
|
## remote-fire replay so all bullets look on-theme.
|
|
static func spawn_bolt(scene: Node, origin: Vector3, target: Vector3,
|
|
color: Color = Color(1.0, 0.72, 0.15)) -> void:
|
|
if scene == null or origin.distance_squared_to(target) <= 0.1:
|
|
return
|
|
var tracer := HitscanTracer.new()
|
|
|
|
var core := MeshInstance3D.new()
|
|
var core_mesh := CapsuleMesh.new()
|
|
core_mesh.radius = 0.025
|
|
core_mesh.height = 1.1
|
|
var m := StandardMaterial3D.new()
|
|
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
m.albedo_color = color.lightened(0.35)
|
|
m.emission_enabled = true
|
|
m.emission = color
|
|
m.emission_energy_multiplier = 2.2
|
|
core_mesh.material = m
|
|
core.mesh = core_mesh
|
|
core.rotation.x = deg_to_rad(90) # orient capsule along Z
|
|
core.position = Vector3(0, 0, -0.55) # tail sits at the origin
|
|
core.material_overlay = LevelMaterials.outline(0.004)
|
|
tracer.add_child(core)
|
|
|
|
tracer.position = origin
|
|
tracer.target_position = target
|
|
scene.add_child(tracer)
|
|
var up := Vector3.UP
|
|
if absf((target - origin).normalized().dot(up)) > 0.99:
|
|
up = Vector3.RIGHT
|
|
tracer.look_at(target, up)
|
|
|
|
func _ready() -> void:
|
|
max_distance = global_position.distance_to(target_position)
|
|
if max_distance < 0.1:
|
|
queue_free()
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
var move_dist = speed * delta
|
|
distance_traveled += move_dist
|
|
|
|
if distance_traveled >= max_distance:
|
|
# Reached target
|
|
queue_free()
|
|
else:
|
|
global_position += -global_transform.basis.z * move_dist
|