Files
Papay-Shooter/weapons/hitscan_tracer.gd
Nicholas ButzkeandClaude Opus 4.8 af8c9831c9 feat: thinner cel outline + procedural model actually holds its weapon
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]>
2026-07-21 17:32:37 -04:00

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