class_name RoomElement
extends StaticBody2D

# Un objet posé dans une salle de donjon.
#
# Seize objets étaient écrits dans l'outil — coffres, pièges, décors, tas à
# casser — et cent quatre-vingt-un placements les répartissaient dans les
# salles. Le donjon n'en faisait rien: la donnée existait, l'objet n'existait
# pas. Ce nœud est ce qui manquait entre les deux.
#
# Il ne décide de rien. Son genre, sa forme, sa vie, son butin et ses crochets
# viennent tous de `room_elements`; ce fichier ne fait qu'obéir.
#
# Ce que chaque genre veut dire:
#
#   decor         il est là, c'est tout. Il bloque le passage s'il est solide.
#   destructible  il a de la vie, encaisse les coups, et lâche son butin en
#                 tombant.
#   chest         il s'ouvre d'un geste, une seule fois, et rend son contenu.
#   trap          il guette dans son rayon, blesse qui entre, puis se réarme.
#   npc, shop     il s'aborde d'un geste et lance son crochet `on_triggered`.
#                 C'est la sortie prévue par la donnée: une scène d'histoire ou
#                 une boutique se branchent là, sans qu'un dialogue soit inventé
#                 ici.

const PICKUP_SCENE := preload("res://scenes/items/ingredient_pickup.tscn")

# Le rayon d'un objet de taille 1. Les créatures partent de 18: un objet posé au
# sol se lit un peu plus large, sinon un coffre a l'air d'un caillou.
const BASE_RADIUS := 22.0
# Ce qui bloque le passage vit sur la couche des corps attaquables, comme une
# créature. Ce qui ne bloque pas vit sur une couche que personne ne masque: les
# coups l'y trouvent quand même, mais le joueur lui marche dessus.
const SOLID_LAYER := 2
const WALKABLE_LAYER := 8

var element_id := ""
var kind := "decor"
var body_shape := "circle"
var body_color := Color("8a6a3d")
var size_scale := 1.0
var health := 0.0
var max_health := 0.0
var contact_damage := 0.0
var trigger_radius := 0.0
var rearm_delay := 0.0
var coins := 0
var solid := true
var spent := false
# Ce qu'il reste avant qu'un piège puisse se redéclencher. Zéro veut dire armé.
var rearm_remaining := 0.0

var _collision: CollisionShape2D

func _ready() -> void:
	add_to_group("room_element")
	set_process(false)

func setup(id: String) -> void:
	element_id = id
	var definition: Dictionary = DataCatalog.ROOM_ELEMENTS.get(id, {})
	if definition.is_empty():
		queue_free()
		return
	kind = str(definition.get("kind", "decor"))
	body_shape = str(definition.get("shape", "circle"))
	body_color = Color(str(definition.get("color", "#8a6a3d")))
	size_scale = maxf(0.1, float(definition.get("size", 1.0)))
	max_health = maxf(0.0, float(definition.get("health", 0.0)))
	health = max_health
	contact_damage = maxf(0.0, float(definition.get("damage", 0.0)))
	trigger_radius = maxf(0.0, float(definition.get("trigger_radius", 0.0)))
	rearm_delay = maxf(0.0, float(definition.get("rearm_delay", 0.0)))
	coins = maxi(0, int(definition.get("coins", 0)))
	solid = bool(definition.get("solid", true))
	_apply_body()
	# Un piège regarde autour de lui; le reste attend qu'on vienne à lui.
	set_process(kind == "trap" and trigger_radius > 0.0)
	# Le joueur cherche ses cibles dans ce groupe: un objet qui n'offre aucun
	# geste n'a rien à y faire, et encombrerait la sélection contextuelle.
	if interaction_action().is_empty():
		remove_from_group("interactable")
	else:
		add_to_group("interactable")
	GameState.add_entity_counter("room_elements", element_id, "total_spawned")
	queue_redraw()

func body_radius() -> float:
	return BASE_RADIUS * size_scale

# La hurtbox d'un objet est son corps: il ne regarde nulle part, donc il n'a pas
# de capsule orientée. Répondre garde les attaques sur le même chemin que pour
# une créature.
func hurtbox_radius() -> float:
	return body_radius()

func hurtbox_half_length() -> float:
	return 0.0

func _apply_body() -> void:
	if _collision == null:
		_collision = CollisionShape2D.new()
		add_child(_collision)
	_collision.shape = ShapeFactory.create_collision(body_shape, body_radius())
	collision_layer = SOLID_LAYER if solid else WALKABLE_LAYER
	collision_mask = 0

# --- Ce qui se casse ---------------------------------------------------------

func can_be_damaged() -> bool:
	return kind == "destructible" and max_health > 0.0 and not spent

func take_damage(amount: float, _direction: Vector2 = Vector2.ZERO,
		_knockback: float = 0.0, _source: Node = null) -> void:
	# Un décor encaisse sans broncher: refuser le coup en silence vaut mieux
	# qu'un coffre qui se casse parce qu'on a frappé à côté.
	if not can_be_damaged():
		return
	health = maxf(0.0, health - maxf(0.0, amount))
	queue_redraw()
	if health <= 0.0:
		_break_open("destroyed")

# --- Ce qui s'ouvre ----------------------------------------------------------

# Ce que le joueur peut faire de cet objet, ou rien du tout. Le donjon lit ça
# pour proposer son geste, exactement comme le restaurant lit ses meubles.
func interaction_action() -> String:
	if spent:
		return ""
	match kind:
		"chest": return "open_element"
		"npc", "shop": return "talk_element"
		_: return ""

# Le joueur appelle `interact(self)`: la signature suit la sienne, sinon l'appel
# échoue à l'exécution sur un simple désaccord d'arité.
func can_interact(_actor: Node2D = null) -> bool:
	return not interaction_action().is_empty()

func get_interaction_radius() -> float:
	return body_radius() + 90.0

func interact(_actor: Node2D = null) -> bool:
	if spent:
		return false
	match kind:
		"chest":
			_break_open("triggered")
			remove_from_group("interactable")
			return true
		"npc", "shop":
			# Un personnage ou une boutique reste abordable: on ne parle pas à
			# quelqu'un une seule fois dans sa vie.
			Registry.run_hook("room_elements", element_id, "on_triggered", self)
			return true
		_:
			return false

# --- Ce qui se déclenche -----------------------------------------------------

func _process(delta: float) -> void:
	if rearm_remaining > 0.0:
		rearm_remaining = maxf(0.0, rearm_remaining - delta)
		return
	var victim := get_tree().get_first_node_in_group("player") as Node2D
	if victim == null or not is_instance_valid(victim):
		return
	if global_position.distance_to(victim.global_position) > trigger_radius + body_radius():
		return
	_spring(victim)

func _spring(victim: Node2D) -> void:
	if contact_damage > 0.0 and victim.has_method("take_damage"):
		var away := (victim.global_position - global_position).normalized()
		victim.call("take_damage", contact_damage, away, 0.0, self)
	Registry.run_hook("room_elements", element_id, "on_triggered", self)
	queue_redraw()
	# Un piège sans délai de réarmement ne se déclenche qu'une fois: sinon il
	# viderait la vie du joueur en une seconde, image après image.
	if rearm_delay <= 0.0:
		spent = true
		set_process(false)
		return
	rearm_remaining = rearm_delay

# --- Ce qu'il rend -----------------------------------------------------------

# Le contenu part, l'objet reste. Un coffre ouvert et un tas cassé se lisent
# encore au sol: les faire disparaître effacerait la trace de ce qu'on a fait.
func _break_open(state: String) -> void:
	if spent:
		return
	spent = true
	health = 0.0
	if coins > 0:
		GameState.add_coins(coins)
	_scatter_drops()
	if state == "destroyed":
		GameState.add_entity_counter("room_elements", element_id, "total_destroyed")
		Registry.run_hook("room_elements", element_id, "on_destroyed", self)
	else:
		Registry.run_hook("room_elements", element_id, "on_triggered", self)
	# Ce qui bloquait le passage continue de le bloquer: une caisse éventrée est
	# toujours une caisse. Seul son contenu a changé de main.
	queue_redraw()

func _scatter_drops() -> void:
	var drops: Array = DataCatalog.ROOM_ELEMENTS.get(element_id, {}).get("drops", [])
	var count_modifier := float(GameState.resolve_game_variable("drop_count_multiplier"))
	var angle := 0.0
	for drop: Dictionary in drops:
		var ingredient_id := str(drop.get("ingredient_id", ""))
		if ingredient_id.is_empty():
			continue
		var attempts := Enemy.drop_attempt_count(count_modifier, randf())
		for _attempt: int in range(attempts):
			if not Enemy.drop_roll_succeeds(float(drop.get("chance", 0.0)), randf()):
				continue
			var pickup := PICKUP_SCENE.instantiate()
			pickup.setup(ingredient_id)
			pickup.position = position + Vector2.from_angle(angle) * (body_radius() + 6.0)
			get_parent().add_child(pickup)
			angle += 1.8

# --- Ce qu'on en voit --------------------------------------------------------

func _draw() -> void:
	# Le pont 3D éteint ce dessin quand il prend la main; en 2D il reste la seule
	# façon de voir l'objet.
	if Render3D.active:
		return
	var tint := body_color
	if spent:
		tint = body_color.darkened(0.45)
	var radius := body_radius()
	if body_shape == "square":
		draw_rect(Rect2(-Vector2.ONE * radius, Vector2.ONE * radius * 2.0), tint)
	else:
		draw_circle(Vector2.ZERO, radius, tint)
	if can_be_damaged() and health < max_health:
		var ratio := clampf(health / maxf(1.0, max_health), 0.0, 1.0)
		draw_rect(Rect2(Vector2(-radius, -radius - 8.0), Vector2(radius * 2.0 * ratio, 3.0)),
			Color("ef233c"))
