class_name TeamHUD extends Control #element de HUD (en bas a gauche) : # - cercle avec l'icone du pokemon selectionne, a cote : niveau, nom, barre de PV et PV chiffres # - dessous : une icone ronde par pokemon de l'equipe, avec son sprite a l'interieur # (option show_empty_slots : afficher aussi les emplacements vides, sous forme de Poke Ball vide) # * l'icone du pokemon selectionne (celui affiche au-dessus) est legerement grisee # * l'icone d'un pokemon K.O. est rouge # * les statuts (brulure, sommeil...) s'affichent en pastille sur la balle #la selection est stockee dans GameData (GameData.get_selected_pokemon()). # #usage : ajouter TeamHUD.new() a ton HUD (CanvasLayer), ou attacher ce script a un Control plein ecran. const ControllerIconTextureScript = preload("res://addons/controller_icons/objects/ControllerIconTexture.gd") #pastilles de statut : [texte, couleur] -- a remplacer par des icones quand tu en auras const STATUS_BADGES : Dictionary = { PokemonInstance.Status.BURN: ["BRU", Color(0.95, 0.45, 0.2)], PokemonInstance.Status.FREEZE: ["GEL", Color(0.45, 0.8, 0.95)], PokemonInstance.Status.PARALYSIS: ["PAR", Color(0.95, 0.85, 0.2)], PokemonInstance.Status.POISON: ["PSN", Color(0.7, 0.4, 0.85)], PokemonInstance.Status.BAD_POISON: ["TOX", Color(0.5, 0.25, 0.7)], PokemonInstance.Status.SLEEP: ["SOM", Color(0.6, 0.65, 0.75)], } const COLOR_SELECTED : Color = Color(0.6, 0.6, 0.6) #pokemon selectionne : balle grisee const COLOR_FAINTED : Color = Color(1.0, 0.4, 0.4) #actions creees automatiquement si elles n'existent pas (croix gauche/droite, fleches du clavier) @export var prev_action : StringName = &"hud_team_prev" @export var next_action : StringName = &"hud_team_next" @export var show_empty_slots : bool = false #false : seulement les icones des pokemon de l'equipe ; true : toujours 6 icones (les vides en Poke Ball pale) @export var show_input_hints : bool = true #icones de touches de part et d'autre des icones @export var portrait_size : float = 120.0 @export var ball_size : float = 52.0 @export var bar_width : float = 220.0 var _root_box : MarginContainer var _info_box : HBoxContainer #portrait + infos (cache s'il n'y a personne de selectionnable) var _portrait_sprite : TextureRect var _level_label : Label var _name_label : Label var _hp_bar : ProgressBar var _hp_fill : StyleBoxFlat var _hp_label : Label var _prev_icon : TextureRect var _next_icon : TextureRect var _balls : Array[BallIcon] = [] var _ball_signatures : Array = [] var _info_signature : Array = [] #---------- elements dessines ---------- class BallIcon extends Control: var color : Color = Color.WHITE #teinte de l'anneau et du sprite (blanc / gris = selectionne / rouge = K.O.) var empty : bool = false var sprite : Texture2D = null var badge_text : String = "" var badge_color : Color = Color.WHITE func _init() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE func _draw() -> void: var center : Vector2 = size * 0.5 var radius : float = minf(size.x, size.y) * 0.5 - 4.0 var c : Color = color if sprite == null: #emplacement vide (ou espece sans sprite) : Poke Ball if empty: c.a = 0.25 draw_arc(center, radius, 0.0, TAU, 40, c, 3.0, true) draw_line(center + Vector2(-radius, 0.0), center + Vector2(radius, 0.0), c, 3.0, true) draw_circle(center, radius * 0.3, c) else: #sprite du pokemon dans un disque sombre, entoure de l'anneau draw_circle(center, radius, Color(0, 0, 0, 0.45)) var box : float = radius * 1.6 var tex_size : Vector2 = sprite.get_size() var fit : float = minf(box / tex_size.x, box / tex_size.y) var draw_size : Vector2 = tex_size * fit draw_texture_rect(sprite, Rect2(center - draw_size * 0.5, draw_size), false, c) draw_arc(center, radius, 0.0, TAU, 40, c, 3.0, true) if badge_text != "": var badge_pos : Vector2 = center + Vector2(radius * 0.75, radius * 0.75) draw_circle(badge_pos, 11.0, badge_color) draw_arc(badge_pos, 11.0, 0.0, TAU, 20, Color(0, 0, 0, 0.7), 1.5, true) draw_string(ThemeDB.fallback_font, badge_pos + Vector2(-11.0, 3.5), badge_text, HORIZONTAL_ALIGNMENT_CENTER, 22.0, 9, Color.BLACK) class PortraitFrame extends Control: func _init() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE func _draw() -> void: var center : Vector2 = size * 0.5 var radius : float = minf(size.x, size.y) * 0.5 - 3.0 draw_circle(center, radius, Color(0, 0, 0, 0.5)) draw_arc(center, radius, 0.0, TAU, 64, Color(1, 1, 1, 0.95), 4.0, true) #---------- initialisation ---------- func _ready() -> void: _ensure_actions() _build_ui() func _ensure_actions() -> void: _ensure_action(prev_action, [_joy_event(JOY_BUTTON_DPAD_LEFT), _key_event(KEY_LEFT)]) _ensure_action(next_action, [_joy_event(JOY_BUTTON_DPAD_RIGHT), _key_event(KEY_RIGHT)]) func _ensure_action(action : StringName, events : Array[InputEvent]) -> void: if InputMap.has_action(action): return InputMap.add_action(action) for e in events: InputMap.action_add_event(action, e) func _joy_event(button : JoyButton) -> InputEvent: var e := InputEventJoypadButton.new() e.button_index = button return e func _key_event(key : Key) -> InputEvent: var e := InputEventKey.new() e.physical_keycode = key return e func _unhandled_input(event : InputEvent) -> void: #n'est pas appele quand le jeu est en pause (menu pause ouvert) if event.is_action_pressed(prev_action): GameData.cycle_selection(-1) get_viewport().set_input_as_handled() elif event.is_action_pressed(next_action): GameData.cycle_selection(1) get_viewport().set_input_as_handled() #---------- construction ---------- func _build_ui() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) mouse_filter = Control.MOUSE_FILTER_IGNORE _root_box = MarginContainer.new() _root_box.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) _root_box.grow_vertical = Control.GROW_DIRECTION_BEGIN #grandit vers le haut _root_box.mouse_filter = Control.MOUSE_FILTER_IGNORE for side in ["left", "bottom"]: _root_box.add_theme_constant_override("margin_" + side, 32) add_child(_root_box) var column := VBoxContainer.new() column.mouse_filter = Control.MOUSE_FILTER_IGNORE column.add_theme_constant_override("separation", 8) _root_box.add_child(column) _build_info(column) _build_ball_row(column) func _build_info(parent : Control) -> void: _info_box = HBoxContainer.new() _info_box.mouse_filter = Control.MOUSE_FILTER_IGNORE _info_box.add_theme_constant_override("separation", 14) parent.add_child(_info_box) #cercle + icone du pokemon (l'icone est inscrite dans le cercle) var frame := PortraitFrame.new() frame.custom_minimum_size = Vector2(portrait_size, portrait_size) _info_box.add_child(frame) _portrait_sprite = TextureRect.new() _portrait_sprite.mouse_filter = Control.MOUSE_FILTER_IGNORE _portrait_sprite.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _portrait_sprite.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED frame.add_child(_portrait_sprite) _portrait_sprite.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT, Control.PRESET_MODE_MINSIZE, int(portrait_size * 0.14)) #niveau, nom, PV var texts := VBoxContainer.new() texts.mouse_filter = Control.MOUSE_FILTER_IGNORE texts.alignment = BoxContainer.ALIGNMENT_CENTER texts.add_theme_constant_override("separation", 2) _info_box.add_child(texts) _level_label = _make_text_label(20) texts.add_child(_level_label) _name_label = _make_text_label(28) texts.add_child(_name_label) var hp_row := HBoxContainer.new() hp_row.mouse_filter = Control.MOUSE_FILTER_IGNORE hp_row.add_theme_constant_override("separation", 12) texts.add_child(hp_row) _hp_bar = ProgressBar.new() _hp_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE _hp_bar.show_percentage = false _hp_bar.custom_minimum_size = Vector2(bar_width, 14) _hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER var track := StyleBoxFlat.new() #la partie "PV perdus" reste visible, plus claire track.bg_color = Color(1, 1, 1, 0.28) track.set_corner_radius_all(7) _hp_fill = StyleBoxFlat.new() _hp_fill.bg_color = Color(0.3, 0.8, 0.35) _hp_fill.set_corner_radius_all(7) _hp_bar.add_theme_stylebox_override("background", track) _hp_bar.add_theme_stylebox_override("fill", _hp_fill) hp_row.add_child(_hp_bar) _hp_label = _make_text_label(20) hp_row.add_child(_hp_label) func _build_ball_row(parent : Control) -> void: var row := HBoxContainer.new() row.mouse_filter = Control.MOUSE_FILTER_IGNORE row.add_theme_constant_override("separation", 4) parent.add_child(row) _prev_icon = _make_hint_icon(prev_action) row.add_child(_prev_icon) for i in PokemonTeam.MAX_SIZE: var ball := BallIcon.new() ball.custom_minimum_size = Vector2(ball_size, ball_size) row.add_child(ball) _balls.append(ball) _ball_signatures.append(null) _next_icon = _make_hint_icon(next_action) row.add_child(_next_icon) func _make_text_label(font_size : int) -> Label: var l := Label.new() l.mouse_filter = Control.MOUSE_FILTER_IGNORE l.add_theme_font_size_override("font_size", font_size) l.add_theme_constant_override("outline_size", 6) l.add_theme_color_override("font_outline_color", Color.BLACK) return l func _make_hint_icon(action : StringName) -> TextureRect: #icone de touche qui s'adapte au peripherique (addon controller_icons) var tr := TextureRect.new() tr.mouse_filter = Control.MOUSE_FILTER_IGNORE var tex : Texture2D = ControllerIconTextureScript.new() tex.set("path", String(action)) tr.texture = tex tr.custom_minimum_size = Vector2(28, 28) tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED tr.size_flags_vertical = Control.SIZE_SHRINK_CENTER return tr #---------- mise a jour (l'equipe change souvent : on lit l'etat chaque frame, sans signaux a rebrancher) ---------- func _process(_delta : float) -> void: var team : PokemonTeam = GameData.player_team GameData.validate_selection() var selected_index : int = GameData.selected_index _root_box.visible = !team.is_empty() var able_count : int = 0 for i in _balls.size(): var p : PokemonInstance = team.get_pokemon(i) if p != null and !p.is_fainted(): able_count += 1 _balls[i].visible = p != null or show_empty_slots #une icone par pokemon (+ les vides si demande) var sig : Array = [p.get_instance_id() if p else 0, p.is_fainted() if p else false, i == selected_index, p.status if p else 0] if _ball_signatures[i] == sig: continue _ball_signatures[i] = sig _apply_ball(_balls[i], p, i == selected_index) var show_hints : bool = show_input_hints and able_count > 1 #seulement s'il y a un choix a faire _prev_icon.visible = show_hints _next_icon.visible = show_hints _update_info(team.get_pokemon(selected_index)) func _apply_ball(ball : BallIcon, p : PokemonInstance, is_selected : bool) -> void: if p == null: ball.empty = true ball.sprite = null ball.color = Color.WHITE ball.badge_text = "" ball.queue_redraw() return ball.empty = false ball.sprite = p.species.get_menu_sprite(false) if p.is_fainted(): ball.color = COLOR_FAINTED elif is_selected: ball.color = COLOR_SELECTED else: ball.color = Color.WHITE var badge : Variant = STATUS_BADGES.get(p.status) if badge != null: ball.badge_text = badge[0] ball.badge_color = badge[1] else: ball.badge_text = "" ball.queue_redraw() func _update_info(p : PokemonInstance) -> void: _info_box.visible = p != null if p == null: _info_signature = [] return var sig : Array = [p.get_instance_id(), p.level, p.current_hp, p.get_max_hp()] if _info_signature == sig: return _info_signature = sig var max_hp : int = p.get_max_hp() _portrait_sprite.texture = p.species.get_menu_sprite(false) _level_label.text = "Lv. %d" % p.level _name_label.text = p.get_display_name() _hp_label.text = "%d/%d" % [p.current_hp, max_hp] _hp_bar.max_value = max_hp _hp_bar.value = p.current_hp var ratio : float = float(p.current_hp) / float(max_hp) _hp_fill.bg_color = Color(0.3, 0.8, 0.35) if ratio > 0.5 else (Color(0.95, 0.7, 0.2) if ratio > 0.2 else Color(0.9, 0.25, 0.25))