class_name Vfx
extends Node2D

# Un effet visuel ponctuel: il se joue une fois, à un endroit, puis se libère
# tout seul. C'est le seul écart avec un visuel d'état, qui lui accompagne une
# entité aussi longtemps qu'elle vit.
#
# Rien de neuf côté déclencheur: les clés d'une carte `vfx` sont les crochets
# que la collection déclare déjà. Un effet visuel part donc exactement là où un
# effet déclaratif part, et il n'y a aucun second vocabulaire à tenir.
#
# Le visuel lui-même vient de la collection `visuals`, sans distinction: une
# gerbe est une feuille de sprites ou une scène, comme une marche.

# Durée par défaut d'une scène, qui ne dit pas combien de temps elle dure.
const SCENE_SECONDS := 1.0
# Durée par défaut d'une feuille qui boucle: sans fin naturelle, il en faut une.
const LOOPING_SECONDS := 1.0

var _remaining := 0.0

# Identifiant du visuel déclaré pour ce crochet, ou "" si la donnée ne dit rien.
static func visual_for(collection: String, entity_id: String, hook: String) -> String:
	var definition: Dictionary = AddressCatalog.collection_data(collection).get(entity_id, {})
	var map: Variant = definition.get("vfx", {})
	if not map is Dictionary:
		return ""
	var visual_id := str((map as Dictionary).get(hook, ""))
	return visual_id if VisualCatalog.is_available(visual_id) else ""

# Combien de temps l'effet reste à l'écran. Zéro déclaré veut dire « déduis-le »,
# comme partout ailleurs dans le projet.
static func lifetime_of(visual_id: String) -> float:
	var definition := VisualCatalog.definition(visual_id)
	if definition.is_empty():
		return 0.0
	var declared := float(definition.get("lifetime", 0.0))
	if declared > 0.0:
		return declared
	if str(definition.get("kind", "frames")) == "scene":
		return SCENE_SECONDS
	if bool(definition.get("loop", true)):
		return LOOPING_SECONDS
	var fps := float(definition.get("fps", 8.0))
	if fps <= 0.0:
		return LOOPING_SECONDS
	return maxf(0.05, float(definition.get("count", 1)) / fps)

# Joue l'effet déclaré pour ce crochet. Rend le noeud créé, ou `null` quand la
# donnée ne déclare rien — c'est un état normal, et l'appelant garde alors son
# propre rendu.
#
# L'effet s'attache au **parent** de l'origine, jamais à l'origine elle-même:
# une gerbe de mort doit survivre au cadavre qui la déclenche.
static func play(collection: String, entity_id: String, hook: String, origin: Node2D, offset: Vector2 = Vector2.ZERO) -> Vfx:
	if origin == null or not is_instance_valid(origin) or not origin.is_inside_tree():
		return null
	var visual_id := visual_for(collection, entity_id, hook)
	if visual_id.is_empty():
		return null
	var host := origin.get_parent()
	if host == null or not is_instance_valid(host):
		return null
	return play_visual(visual_id, host, origin.global_position + offset, origin.z_index + 1)

# Variante directe pour le lecteur de scenes: le scenario connait deja le
# visuel et peut choisir combien de temps attendre sans inventer un crochet.
static func play_visual(visual_id: String, host: Node, world_position: Vector2, draw_order: int = 1, lifetime_override: float = 0.0) -> Vfx:
	if visual_id.is_empty() or not VisualCatalog.is_available(visual_id) \
	or host == null or not is_instance_valid(host) or not host.is_inside_tree():
		return null
	var node := Vfx.new()
	node.name = "Vfx"
	# Un groupe, pour que ce qui vole a l ecran soit denombrable — le test s en
	# sert, et une pause de plongee pourra s en servir aussi.
	node.add_to_group("vfx")
	node._remaining = lifetime_override if lifetime_override > 0.0 else lifetime_of(visual_id)
	node.z_index = draw_order
	var holder := VisualHolder.new()
	holder.name = "Visuals"
	node.add_child(holder)
	host.add_child(node)
	# Après l'entrée dans l'arbre: la position globale n'a de sens qu'à ce
	# moment-là, et le porteur a besoin d'un arbre pour instancier une scène.
	node.global_position = world_position
	holder.show_visual(visual_id)
	if not holder.is_showing():
		node.queue_free()
		return null
	return node

func _process(delta: float) -> void:
	_remaining -= delta
	if _remaining <= 0.0:
		queue_free()
