extends RefCounted

# Shared 2D authority for authored attack areas. Visuals call the same shape
# vocabulary, but only this geometry decides whether a body is hit.
#
# --- La capsule ---------------------------------------------------------------
#
# Un corps touché n'est pas un point mais une **capsule**: un segment orienté sur
# le regard de la créature, plus un rayon. Vue de dessus, c'est ce qu'un
# assemblage sphère + cylindre donne une fois aplati, en un seul test.
#
# Une longueur nulle ramène le segment à son centre, donc au cercle d'avant: les
# créatures qui ne déclarent pas de capsule gardent **exactement** leurs verdicts.
# C'est la garantie qui permet de poser la capsule sans rejouer l'équilibrage.

# Le point de la capsule le plus proche d'une position donnée. `half_length` est
# la demi-longueur du segment, `forward` son orientation.
static func capsule_closest_point(centre: Vector2, forward: Vector2,
		half_length: float, towards: Vector2) -> Vector2:
	if half_length <= 0.0:
		return centre
	var axis := forward.normalized() if forward.length_squared() > 0.001 else Vector2.DOWN
	var along := clampf(axis.dot(towards - centre), -half_length, half_length)
	return centre + axis * along

static func body_overlaps(area_shape: String, origin: Vector2, direction: Vector2,
		reach: float, angle_degrees: float, area_width: float,
		target_position: Vector2, target_radius: float) -> bool:
	var forward := direction.normalized() if direction.length_squared() > 0.001 else Vector2.DOWN
	var offset := target_position - origin
	if area_shape == "rectangle":
		var along := forward.dot(offset)
		var across := forward.orthogonal().dot(offset)
		var closest := Vector2(
			clampf(along, 0.0, maxf(0.0, reach)),
			clampf(across, -area_width * 0.5, area_width * 0.5)
		)
		return Vector2(along, across).distance_squared_to(closest) <= target_radius * target_radius
	var distance := offset.length()
	if distance - target_radius > reach:
		return false
	if angle_degrees >= 359.5 or distance <= target_radius or distance <= 0.001:
		return true
	var half_arc := deg_to_rad(angle_degrees * 0.5)
	var body_half_angle := asin(clampf(target_radius / distance, 0.0, 1.0))
	var spread := acos(clampf(forward.dot(offset / distance), -1.0, 1.0))
	return spread - body_half_angle <= half_arc

# Même question, posée sur une capsule. Le point du segment le plus proche de
# l'origine de l'attaque est celui qui décide: c'est lui qui entre en premier
# dans la zone, quelle que soit sa forme.
static func capsule_overlaps(area_shape: String, origin: Vector2, direction: Vector2,
		reach: float, angle_degrees: float, area_width: float,
		target_position: Vector2, target_forward: Vector2,
		target_half_length: float, target_radius: float) -> bool:
	var closest := capsule_closest_point(target_position, target_forward, target_half_length, origin)
	return body_overlaps(area_shape, origin, direction, reach, angle_degrees, area_width,
		closest, target_radius)

# Premier contact d'un cercle en vol contre une capsule immobile. Le vol est
# échantillonné sur le segment de la capsule plutôt que l'inverse: le projectile
# reste un cercle, la cible garde sa longueur.
static func segment_capsule_hit_ratio(start: Vector2, finish: Vector2,
		centre: Vector2, forward: Vector2, half_length: float, radius: float) -> float:
	if half_length <= 0.0:
		return segment_circle_hit_ratio(start, finish, centre, radius)
	# Deux extrémités et le point le plus proche du départ couvrent la capsule
	# sans intégrale: un projectile traverse toujours par l'un des trois.
	var axis := forward.normalized() if forward.length_squared() > 0.001 else Vector2.DOWN
	var best := -1.0
	for probe: Vector2 in [
		centre - axis * half_length,
		centre + axis * half_length,
		capsule_closest_point(centre, axis, half_length, start),
		capsule_closest_point(centre, axis, half_length, finish),
	]:
		var ratio := segment_circle_hit_ratio(start, finish, probe, radius)
		if ratio >= 0.0 and (best < 0.0 or ratio < best):
			best = ratio
	return best

static func broad_phase_radius(area_shape: String, reach: float, area_width: float) -> float:
	if area_shape == "rectangle":
		return Vector2(maxf(0.0, reach), maxf(0.0, area_width) * 0.5).length()
	return maxf(0.0, reach)

static func rectangle_points(direction: Vector2, length: float, width: float,
		origin: Vector2 = Vector2.ZERO) -> PackedVector2Array:
	var forward := direction.normalized() if direction.length_squared() > 0.001 else Vector2.DOWN
	var side := forward.orthogonal() * width * 0.5
	return PackedVector2Array([
		origin - side,
		origin + side,
		origin + forward * length + side,
		origin + forward * length - side,
	])

static func closed_points(points: PackedVector2Array) -> PackedVector2Array:
	var closed := points.duplicate()
	if not closed.is_empty():
		closed.append(closed[0])
	return closed

static func movement_direction(movement_type: String, attack_direction: Vector2,
		strafe_sign: float) -> Vector2:
	var direction := attack_direction.normalized() if attack_direction.length_squared() > 0.001 else Vector2.DOWN
	match movement_type:
		"retreat":
			return -direction
		"strafe":
			return direction.orthogonal() * strafe_sign
	return direction

# First contact of a swept circle against a stationary circle, expressed on the
# segment [0, 1]. Returning -1 keeps callers allocation-free and makes it easy
# to sort every crossed enemy in flight order, preventing tunnelling.
static func segment_circle_hit_ratio(start: Vector2, finish: Vector2,
		centre: Vector2, radius: float) -> float:
	var segment := finish - start
	var offset := start - centre
	var a := segment.length_squared()
	if a <= 0.000001:
		return 0.0 if offset.length_squared() <= radius * radius else -1.0
	var b := 2.0 * offset.dot(segment)
	var c := offset.length_squared() - radius * radius
	var discriminant := b * b - 4.0 * a * c
	if discriminant < 0.0:
		return -1.0
	var root := sqrt(discriminant)
	var first := (-b - root) / (2.0 * a)
	var second := (-b + root) / (2.0 * a)
	if first >= 0.0 and first <= 1.0:
		return first
	if second >= 0.0 and second <= 1.0:
		return second
	return 0.0 if c <= 0.0 else -1.0

# A nearer body occludes a farther target only when the sight segment actually
# crosses its physical radius. Side-by-side enemies therefore remain hittable
# by a wide cone; bodies aligned behind one another consume piercing depth.
static func body_occludes(origin: Vector2, target: Vector2,
		blocker: Vector2, blocker_radius: float) -> bool:
	var to_target := target - origin
	var length_squared := to_target.length_squared()
	if length_squared <= 0.001:
		return false
	var ratio := clampf((blocker - origin).dot(to_target) / length_squared, 0.0, 1.0)
	if ratio <= 0.0 or ratio >= 1.0:
		return false
	var closest := origin + to_target * ratio
	return closest.distance_squared_to(blocker) <= blocker_radius * blocker_radius
