class_name SentPokemon extends Node3D #un pokemon de l'equipe sorti dans le monde (apres un lancer). #plusieurs peuvent etre sortis en meme temps ; un meme pokemon ne peut l'etre qu'une fois. signal recalled const GROUP : StringName = &"sent_pokemon" const MODEL_FACES_PLUS_Z : bool = true #true si tes modeles regardent vers +Z (cas des glTF) : sinon mets false const HEIGHT_TO_METERS : float = 1.0 #PokeAPI donne la taille en decimetres ; mets 1.0 si ton import l'a deja convertie en metres const MIN_HEIGHT : float = 0.3 #bornes de la capsule en metres (augmente MAX_HEIGHT pour une echelle fidele aux tres gros pokemon) const MAX_HEIGHT : float = 8.0 const DEFAULT_HEIGHT : float = 1.0 #utilise si la taille n'est pas renseignee (0) static var _counter : int = 0 #ordre d'envoi, pour retrouver le plus recent var pokemon : PokemonInstance var model : Node3D var order : int = 0 var rider : Node3D = null static func send(parent : Node, p : PokemonInstance, at : Vector3, look_at_pos : Vector3 = Vector3.INF) -> SentPokemon: #un pokemon deja sorti est d'abord rappele (il "se deplace" a l'endroit du nouveau lancer) for existing in parent.get_tree().get_nodes_in_group(GROUP): if existing is SentPokemon and existing.pokemon == p: existing.recall() var sent := SentPokemon.new() sent.pokemon = p sent.order = _counter _counter += 1 parent.add_child(sent) sent.global_position = at sent._build_model() if look_at_pos != Vector3.INF: var flat : Vector3 = Vector3(look_at_pos.x, at.y, look_at_pos.z) if flat.distance_to(at) > 0.01: sent.look_at(flat, Vector3.UP) if MODEL_FACES_PLUS_Z: sent.rotate_y(PI) #look_at oriente -Z vers la cible return sent static func find_for(tree : SceneTree, p : PokemonInstance) -> SentPokemon: #le SentPokemon de ce pokemon, s'il est actuellement dehors for n in tree.get_nodes_in_group(GROUP): if n is SentPokemon and n.pokemon == p and !n.is_queued_for_deletion(): return n return null static func get_latest(tree : SceneTree) -> SentPokemon: #le pokemon sorti le plus recemment var latest : SentPokemon = null for n in tree.get_nodes_in_group(GROUP): if n is SentPokemon and !n.is_queued_for_deletion() and (latest == null or n.order > latest.order) and n.rider == null: latest = n return latest func _ready() -> void: add_to_group(GROUP) func _build_model() -> void: model = create_model(pokemon) add_child(model) static func create_model(p : PokemonInstance) -> Node3D: #modele 3D de l'espece ; capsule coloree selon le type s'il n'y en a pas var result : Node3D = null var scene : PackedScene = p.species.model_scene if scene: result = scene.instantiate() as Node3D if result == null: result = _create_placeholder(p) return result static func _create_placeholder(p : PokemonInstance) -> Node3D: var colors : Array[Color] = TypeColors.for_species(p.species) #taille issue des donnees de l'espece ; le rayon suit la hauteur pour garder des proportions coherentes var meters : float = p.species.height * HEIGHT_TO_METERS if p.species != null else 0.0 var height : float = clampf(meters if meters > 0.0 else DEFAULT_HEIGHT, MIN_HEIGHT, MAX_HEIGHT) var radius : float = clampf(height * 0.3, 0.12, 1.5) var body := MeshInstance3D.new() var capsule := CapsuleMesh.new() capsule.radius = radius capsule.height = maxf(height, radius * 2.0) #une capsule ne peut pas etre plus basse que son diametre body.mesh = capsule body.position.y = capsule.height * 0.5 #les pieds posent sur le sol body.material_override = _make_material(colors[0]) #deuxieme type : anneau autour de la capsule if colors.size() > 1: var ring := MeshInstance3D.new() var torus := TorusMesh.new() torus.inner_radius = radius * 0.97 torus.outer_radius = radius * 1.2 ring.mesh = torus ring.material_override = _make_material(colors[1]) body.add_child(ring) return body static func _make_material(color : Color) -> StandardMaterial3D: var mat := StandardMaterial3D.new() mat.albedo_color = color mat.roughness = 0.8 return mat func recall() -> void: recalled.emit() queue_free()