extends RefCounted

# Pas de `class_name`: un nom global neuf n'existe qu'apres une passe d'editeur.
# Les appelants preloadent ce script.

## Les capacites speciales (ligne 59): le code de chaque genre du contrat.
##
## Une capacite fait ce qu'aucune donnee generique ne sait decrire. L'auteur en
## configure des exemplaires (collection `abilities`) et les declenche depuis
## la frise d'un pattern ou la fiche d'un monstre; ce script les joue.
##
## Une capacite agit sur le JEU (elle fait apparaitre de vraies creatures, avec
## leur corps et leurs regles), jamais sur le seul dessin. Elle ne decide pas
## quand elle part: la frise ou le declencheur de fiche l'a deja decide.
##
## Ajouter un genre: une branche dans `trigger`, sa fonction ici, puis sa
## declaration dans data/ability_contract.json (que PHP et la page lisent).

## Une creature invoquee peut invoquer a son tour, jusqu'a cette generation.
## Une fiche qui s'invoque elle-meme a la mort ne boucle donc pas sans fin.
const MAX_SUMMON_GENERATION := 3
## Les invocations restent a cette distance des murs de l'arene.
const ARENA_MARGIN := 48.0


## Joue la capacite pour ce lanceur. Rend vrai si elle a agi.
static func trigger(ability_id: String, caster: Node2D) -> bool:
	var ability: Dictionary = DataCatalog.ABILITIES.get(ability_id, {})
	if ability.is_empty() or caster == null or not is_instance_valid(caster) or not caster.is_inside_tree():
		return false
	var params: Dictionary = ability.get("params", {})
	match str(ability.get("kind", "")):
		"summon":
			return summon(caster, params) > 0
		"flee":
			return bool(caster.call("flee", float(params.get("speed_multiplier", 1.6)), bool(params.get("drops_loot", false)))) \
				if caster.has_method("flee") else false
		"companion_gesture":
			return companion_gesture(caster, params)
	push_warning("Capacite '%s': genre '%s' sans code." % [ability_id, str(ability.get("kind", ""))])
	return false


# --- Le geste d'un compagnon (le combat final, phase 10) -------------------------

## Les gestes qui posent un effet sur l'entité, et cet effet (une fiche `effects`).
const GESTURE_EFFECTS := {"claw": "gesture_claw", "swarm": "gesture_swarm", "diversion": "gesture_diversion", "tune": "gesture_tune"}

## Si sa quête est faite, le compagnon intervient : sa réplique en bandeau, puis
## son geste, sur l'entité qui l'a déclenché ou sur le héros. Sinon, rien : son
## seuil passe. Rend vrai s'il a agi.
static func companion_gesture(caster: Node2D, params: Dictionary) -> bool:
	var requires := str(params.get("requires", ""))
	if not DataCatalog.GAME_FACTS.has(requires) or not bool(GameState.get_fact_value(requires)):
		return false
	var gesture := str(params.get("gesture", ""))
	GameState.toast_requested.emit(Loc.ui("ui.roof.gesture." + gesture))
	match gesture:
		"jays":
			caster.call("take_damage", float(caster.get("max_health")) * float(params.get("power", 0.05)))
		"micro":
			if caster.has_method("expose"):
				caster.call("expose", 1.0 + float(params.get("power", 0.5)), float(params.get("seconds", 8.0)))
		"shield", "bob":
			var hero: Node = GameState.scoped_first(caster, "player")
			if hero != null:
				hero.set("invulnerability", maxf(float(hero.get("invulnerability")), float(params.get("seconds", 4.0))))
		_:
			if GESTURE_EFFECTS.has(gesture) and caster.has_method("apply_combat_effects"):
				caster.call("apply_combat_effects", [{"effect": GESTURE_EFFECTS[gesture], "chance": 100.0}], "gesture")
	var marks := str(params.get("marks", "none"))
	if marks != "none" and DataCatalog.GAME_FACTS.has(marks):
		GameState.set_fact_value(marks, true)
	return true


# --- Invoquer des acolytes ------------------------------------------------------

## Fait apparaitre les acolytes et rend leur nombre. Un lanceur encore debout
## les garde: ils comptent dans son plafond et partent avec lui si la capacite
## le dit. Un lanceur qui tombe (capacite « a la mort ») laisse des heritiers
## libres: le lier a eux les ferait disparaitre aussitot.
static func summon(caster: Node2D, params: Dictionary) -> int:
	var host := _summon_host(caster)
	if host == null:
		return 0
	var generation := int(caster.get("summon_generation"))
	if generation >= MAX_SUMMON_GENERATION:
		return 0
	var bound := bool(caster.get("active"))
	var count := int(params.get("count", 1))
	var cap := int(params.get("max_alive", 0))
	if cap > 0 and bound:
		count = mini(count, cap - int(caster.call("living_summon_count")))
	if count <= 0:
		return 0
	var hero: Variant = host.get("player")
	var hero_position: Vector2 = (hero as Node2D).global_position \
		if is_instance_valid(hero) and hero is Node2D else caster.global_position
	var arena := Rect2()
	if host.has_method("get_combat_arena_rect"):
		arena = host.call("get_combat_arena_rect")
	var points := summon_points(str(params.get("placement", "around_caster")), count,
		caster.global_position, hero_position, float(params.get("radius", 140.0)), arena, randf() * TAU)
	var options := {
		"holds_room": bool(params.get("holds_room", true)),
		"generation": generation + 1,
		"drops": bool(params.get("drops_loot", false)),
	}
	var made := 0
	for point: Vector2 in points:
		var creature: Variant = host.call("summon_creature", str(params.get("creature", "")), point, options)
		if creature == null:
			continue
		if bound:
			caster.call("adopt_summon", creature, bool(params.get("die_with_caster", true)))
		made += 1
	return made


## Ou poser `count` acolytes. Fonction pure: le hasard n'entre que par l'angle
## de depart, que l'appelant tire.
static func summon_points(placement: String, count: int, caster_position: Vector2,
		hero_position: Vector2, radius: float, arena: Rect2, start_angle: float) -> Array[Vector2]:
	var points: Array[Vector2] = []
	var inner := arena.grow(-ARENA_MARGIN) if arena.size.x > ARENA_MARGIN * 2.0 and arena.size.y > ARENA_MARGIN * 2.0 else arena
	for index: int in range(count):
		var turn := start_angle + TAU * float(index) / float(maxi(1, count))
		var point: Vector2
		match placement:
			"around_hero":
				point = hero_position + Vector2.from_angle(turn) * radius
			"room_edges":
				point = _edge_point(inner, fposmod(turn / TAU, 1.0)) if inner.has_area() \
					else caster_position + Vector2.from_angle(turn) * radius
			_:
				point = caster_position + Vector2.from_angle(turn) * radius
		if inner.has_area():
			point = point.clamp(inner.position, inner.end)
		points.append(point)
	return points


## Un point du pourtour de l'arene, `ratio` de 0 a 1 en faisant le tour.
static func _edge_point(rect: Rect2, ratio: float) -> Vector2:
	var perimeter := 2.0 * (rect.size.x + rect.size.y)
	var walk := ratio * perimeter
	if walk < rect.size.x:
		return rect.position + Vector2(walk, 0.0)
	walk -= rect.size.x
	if walk < rect.size.y:
		return rect.position + Vector2(rect.size.x, walk)
	walk -= rect.size.y
	if walk < rect.size.x:
		return rect.end - Vector2(walk, 0.0)
	walk -= rect.size.x
	return Vector2(rect.position.x, rect.end.y - walk)


## Le donjon qui porte le lanceur: lui seul sait faire apparaitre une creature
## et la compter dans la salle.
static func _summon_host(caster: Node) -> Node:
	var node := caster.get_parent()
	while node != null:
		if node.has_method("summon_creature"):
			return node
		node = node.get_parent()
	return null
