class_name SpearProjectile
extends RigidBody3D

# A thrown spear. It flies a ballistic arc (gravity does the work — the launch
# velocity is solved by the matador to land on the bull) and keeps its shaft aimed
# along its own velocity, so it noses over at the apex and drives in point-first.
# On impact it embeds where it struck: the tip buried just past the contact point
# with the shaft trailing back along the flight line, so it reads as stuck-in rather
# than floating alongside. A hit on the bull leaves the spear riding it like a spine
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
# it in place until the despawn timer clears it.

const _DESPAWN_SEC: float = 8.0
const _STUCK_GROUP: StringName = &"stuck_spear"
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
const _TIP_Z: float = 1.915
const _EMBED_DEPTH: float = 0.45  # how far past the surface the tip buries

var _mesh: Node3D = null
var _stopped: bool = false
var _flight_dir: Vector3 = Vector3.ZERO  # last pre-impact travel direction


# Build, seat and launch a spear from an already-instantiated mesh (the one that was
# in the matador's hand). `origin` is the release point; `velocity` is the full
# ballistic launch vector; `gravity` is the RigidBody gravity_scale for the arc.
static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector3,
		gravity: float) -> SpearProjectile:
	var s := SpearProjectile.new()
	s.gravity_scale = gravity
	s.collision_layer = 0
	s.collision_mask = 1
	s.contact_monitor = true
	s.max_contacts_reported = 6
	parent.add_child(s)
	# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
	s.global_transform = Transform3D(_aim_basis(velocity), origin)
	mesh.reparent(s, false)
	mesh.transform = Transform3D.IDENTITY
	s._mesh = mesh
	s.add_child(_shaft_collider())
	s.linear_velocity = velocity
	_despawn(s)
	return s


# Shed every spear stuck in `bull` — pop each off as a tumbling body that flies clear
# and clears itself on the despawn timer. Called by the bull's slam.
static func shed_all(bull: Node) -> void:
	if not (bull is Node3D):
		return
	var scene: Node = bull.get_tree().current_scene
	if scene == null:
		scene = bull
	var center: Vector3 = (bull as Node3D).global_position
	for child in bull.get_children():
		if not child.is_in_group(_STUCK_GROUP):
			continue
		var mesh := child as Node3D
		var gx := mesh.global_transform
		var rb := RigidBody3D.new()
		rb.collision_layer = 0
		rb.collision_mask = 1
		scene.add_child(rb)
		rb.global_transform = gx
		mesh.reparent(rb, false)
		mesh.transform = Transform3D.IDENTITY
		mesh.remove_from_group(_STUCK_GROUP)
		rb.add_child(_shaft_collider())
		# Fling outward from the bull with a bit of lift and spin.
		var out := gx.origin - center
		out.y = 0.0
		out = out.normalized() if out.length() > 0.01 else Vector3(randf() - 0.5, 0.0, randf() - 0.5)
		rb.linear_velocity = out * randf_range(3.0, 6.0) + Vector3.UP * randf_range(3.0, 5.0)
		rb.angular_velocity = Vector3(randf_range(-10.0, 10.0),
			randf_range(-10.0, 10.0), randf_range(-10.0, 10.0))
		_despawn(rb)


static func _shaft_collider() -> CollisionShape3D:
	# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
	# can't tunnel past the bull's collision spheres.
	var cap := CapsuleShape3D.new()
	cap.radius = 0.13
	cap.height = 2.2
	var cs := CollisionShape3D.new()
	cs.shape = cap
	cs.position = Vector3(0.0, 0.0, 0.79)
	cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
	return cs


# Continuous bleed from a spear lodged in the bull: a steady spurt of dark-red gobs out
# of the entry wound. Parented to the stuck mesh so it tracks the wound as the bull
# moves; world-space particles (local_coords off) so they arc off and fall behind as a
# trail. Dies with the mesh when the spear is shed or despawned.
static func _wound_emitter() -> CPUParticles3D:
	var ramp := Gradient.new()
	ramp.set_color(0, Color(0.5, 0.02, 0.02, 1.0))
	ramp.set_color(1, Color(0.22, 0.0, 0.0, 0.0))

	var sc := Curve.new()
	sc.add_point(Vector2(0.0, 1.0))
	sc.add_point(Vector2(0.6, 0.7))
	sc.add_point(Vector2(1.0, 0.0))

	var p := CPUParticles3D.new()
	# Seat at the wound (where the shaft meets the hide) and spray back out along the
	# shaft: mesh local -Z is the exit direction, rotated into world by the node basis.
	p.position = Vector3(0.0, 0.0, _TIP_Z - _EMBED_DEPTH)
	p.emitting = true
	p.amount = 22
	p.lifetime = 0.6
	p.explosiveness = 0.0
	p.randomness = 0.6
	p.local_coords = false
	p.direction = Vector3(0.0, 0.55, -1.0).normalized()
	p.spread = 32.0
	p.gravity = Vector3(0.0, -12.0, 0.0)
	p.initial_velocity_min = 1.6
	p.initial_velocity_max = 4.0
	p.scale_amount_min = 0.5
	p.scale_amount_max = 1.4
	p.mesh = _blood_mesh()
	p.color_ramp = ramp
	p.scale_amount_curve = sc
	return p


# Small unshaded, vertex-coloured sphere for blood gobs.
static func _blood_mesh() -> SphereMesh:
	var mat := StandardMaterial3D.new()
	mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	mat.vertex_color_use_as_albedo = true
	mat.cull_mode = BaseMaterial3D.CULL_DISABLED
	var s := SphereMesh.new()
	s.radius = 0.045
	s.height = 0.09
	s.radial_segments = 4
	s.rings = 2
	s.material = mat
	return s


static func _despawn(node: Node) -> void:
	node.get_tree().create_timer(_DESPAWN_SEC).timeout.connect(func() -> void:
		if is_instance_valid(node):
			node.queue_free()
	)


# Basis whose local +Z points along `v` (looking_at points -Z at its target).
static func _aim_basis(v: Vector3) -> Basis:
	var dir := v.normalized()
	if dir.length_squared() < 0.0001:
		return Basis.IDENTITY
	var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD
	return Basis.looking_at(-dir, up)


# While flying, keep the shaft aligned with the velocity; on the first contact freeze
# and hand off to _embed so the spear sticks where it struck instead of bouncing off.
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
	if _stopped:
		return
	if state.get_contact_count() > 0:
		_stopped = true
		# Use the remembered pre-impact heading — state.linear_velocity is already the
		# bounced (post-collision) velocity by now, which would aim the shaft wrong.
		var contact := state.get_contact_collider_position(0)
		var other := state.get_contact_collider_object(0)
		state.linear_velocity = Vector3.ZERO
		state.angular_velocity = Vector3.ZERO
		_embed.call_deferred(other, contact, _flight_dir)
		return
	var v := state.linear_velocity
	if v.length_squared() > 0.04:
		_flight_dir = v.normalized()
		var t := state.transform
		t.basis = _aim_basis(v)
		state.transform = t
	state.angular_velocity = Vector3.ZERO


# Compose the stuck pose (tip buried _EMBED_DEPTH past the contact, shaft along the
# flight line) and plant the spear. A bull hit hands the mesh to the bull so it rides
# along as a spine; anything else pins the body in place for the despawn timer.
func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
	var f := flight.normalized()
	if f.length_squared() < 0.0001:
		f = global_transform.basis.z.normalized()
	var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
	var bull := other as Node
	if bull != null and bull.is_in_group(&"player"):
		bull.call(&"take_sword_hit", "thrown", contact)
		if is_instance_valid(_mesh) and is_instance_valid(bull):
			_mesh.reparent(bull, false)
			_mesh.global_transform = pose
			_mesh.add_to_group(_STUCK_GROUP)
			if Settings.gore:
				_mesh.add_child(_wound_emitter())  # keep bleeding from the wound while lodged
		queue_free()
		return
	freeze = true
	global_transform = pose
