class_name AttackField3D
extends Node3D

# Le champ d'une attaque, peint au sol.
#
# En 2D, un monstre annonçait son coup en dessinant un secteur devant lui, dans
# ses propres coordonnées. Sous une caméra 3D inclinée ce dessin n'est plus au
# même endroit à l'écran — c'est la raison d'être de `InfoBadge3D` — et la garde
# qui éteint le corps 2D l'emportait avec lui: le joueur ne voyait plus **rien
# venir**.
#
# Le champ remonte donc dans la scène 3D, à plat sur le sol: la place naturelle
# d'une zone d'effet vue de trois quarts, et la seule où sa portée se lit en
# vraie grandeur. Il ne calcule rien — portée, ouverture, direction et couleur
# viennent de l'entité, exactement comme dans `_draw()`. Une règle changée dans
# le jeu se voit ici sans qu'on y touche.

# Assez haut pour passer au-dessus du sol (dalle à 0) et des zones teintées de
# la salle (2), assez bas pour rester une marque au sol et non un mur.
const GROUND_Y := 2.5
const SEGMENTS := 24
const EDGE_WIDTH := 3.0

var _fill: MeshInstance3D
var _edge: MeshInstance3D
var _fill_material: StandardMaterial3D
var _edge_material: StandardMaterial3D
# Ce qui est déjà construit: rayon, ouverture, plein ou creux. Refabriquer un
# maillage identique à chaque frame rendrait le télégraphe plus cher que le
# combat qu'il annonce.
var _built := Vector3.INF

func _ready() -> void:
	# Posé dans le monde, pas dans le repère de l'avatar: celui-ci tourne, et
	# son cap est **lissé** (`lerp_angle`), alors que la direction d'attaque est
	# une donnée à part qui doit être exacte à la frame près. Enfant pour la
	# durée de vie, indépendant pour la pose.
	top_level = true
	_fill_material = _flat_material()
	_edge_material = _flat_material()
	# Nommés: le contrôle mesure la géométrie réellement produite, et l'éditeur
	# les montre par leur rôle quand on inspecte l'arbre.
	_fill = MeshInstance3D.new()
	_fill.name = "Fill"
	_fill.material_override = _fill_material
	add_child(_fill)
	_edge = MeshInstance3D.new()
	_edge.name = "Edge"
	_edge.material_override = _edge_material
	add_child(_edge)
	visible = false

static func _flat_material() -> StandardMaterial3D:
	var material := StandardMaterial3D.new()
	material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	# Une marque au sol se regarde des deux côtés: sans ça elle disparaît dès
	# que la caméra passe de l'autre bord.
	material.cull_mode = BaseMaterial3D.CULL_DISABLED
	return material

# Le secteur: un cône devant l'attaquant, ou le disque entier à 360°.
func show_field(centre: Vector2, direction: Vector2, radius: float, angle_degrees: float, tint: Color) -> void:
	if radius <= 0.5:
		hide_field()
		return
	_rebuild(radius, clampf(angle_degrees, 1.0, 360.0), true)
	_place(centre, direction)
	_paint(tint)
	visible = true

# L'anneau: le coup direct, qui n'a ni portée ni ouverture — juste un contact.
func show_ring(centre: Vector2, radius: float, tint: Color) -> void:
	if radius <= 0.5:
		hide_field()
		return
	_rebuild(radius, 360.0, false)
	_place(centre, Vector2.DOWN)
	_paint(tint)
	visible = true

func hide_field() -> void:
	visible = false

func _place(centre: Vector2, direction: Vector2) -> void:
	position = Shape3DFactory.to_3d(centre, GROUND_Y)
	if direction.length_squared() > 0.0001:
		# Même convention que le cap des avatars: +Z est l'avant.
		rotation.y = atan2(direction.x, direction.y)

func _paint(tint: Color) -> void:
	_fill_material.albedo_color = tint
	var rim := tint.lightened(0.25)
	rim.a = minf(1.0, tint.a + 0.4)
	_edge_material.albedo_color = rim

func _rebuild(radius: float, angle_degrees: float, filled: bool) -> void:
	var signature := Vector3(radius, angle_degrees, 1.0 if filled else 0.0)
	if signature.is_equal_approx(_built):
		return
	_built = signature
	var full := angle_degrees >= 359.5
	var arc := PackedVector3Array()
	var span := deg_to_rad(angle_degrees)
	for index: int in range(SEGMENTS + 1):
		var phi := -span * 0.5 + span * float(index) / float(SEGMENTS)
		arc.append(Vector3(sin(phi) * radius, 0.0, cos(phi) * radius))
	_fill.visible = filled
	if filled:
		_fill.mesh = _fan(arc)
	# Le contour d'un cône passe par sa pointe; celui d'un disque, non.
	var outline := PackedVector3Array()
	if not full:
		outline.append(Vector3.ZERO)
	outline.append_array(arc)
	if not full:
		outline.append(Vector3.ZERO)
	_edge.mesh = _ribbon(outline, EDGE_WIDTH)

static func _fan(arc: PackedVector3Array) -> ArrayMesh:
	var surface := SurfaceTool.new()
	surface.begin(Mesh.PRIMITIVE_TRIANGLES)
	for index: int in range(arc.size() - 1):
		surface.add_vertex(Vector3.ZERO)
		surface.add_vertex(arc[index])
		surface.add_vertex(arc[index + 1])
	return surface.commit()

# Un ruban plat le long d'une ligne: c'est la bordure.
#
# Une primitive `LINE_STRIP` fait **un pixel** de large quelle que soit la
# distance — sur un sol vu de haut, elle s'efface. Le contour est donc de la
# géométrie comme le reste, sinon le secteur n'aurait plus de bord net et sa
# portée redeviendrait une impression.
static func _ribbon(points: PackedVector3Array, width: float) -> ArrayMesh:
	var surface := SurfaceTool.new()
	surface.begin(Mesh.PRIMITIVE_TRIANGLES)
	var half := width * 0.5
	var drawn := false
	for index: int in range(points.size() - 1):
		var from_point := points[index]
		var to_point := points[index + 1]
		var step := to_point - from_point
		# Un segment de longueur nulle donne une normale invalide: le disque
		# ferme sur son point de départ, et deux points confondus suffiraient à
		# produire un maillage troué.
		if step.length_squared() < 0.0001:
			continue
		var side := Vector3(step.z, 0.0, -step.x).normalized() * half
		surface.add_vertex(from_point + side)
		surface.add_vertex(from_point - side)
		surface.add_vertex(to_point + side)
		surface.add_vertex(from_point - side)
		surface.add_vertex(to_point - side)
		surface.add_vertex(to_point + side)
		drawn = true
	if not drawn:
		return ArrayMesh.new()
	return surface.commit()
