diff --git a/game/ui/hud/team_hud.gd b/game/ui/hud/team_hud.gd index 6acd782..9dd94d8 100644 --- a/game/ui/hud/team_hud.gd +++ b/game/ui/hud/team_hud.gd @@ -1,38 +1,111 @@ class_name TeamHUD extends Control -#element de HUD : barre de l'equipe en bas a gauche ; le pokemon selectionne est agrandi. -#la selection est stockee dans GameData (GameData.get_selected_pokemon()), -#pour que le lancer (ThrowState...) sache quel pokemon envoyer. +#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 slot_size : float = 68.0 -@export var selected_slot_size : float = 90.0 +@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 _slot_panels : Array[PanelContainer] = [] -var _slot_styles : Array[StyleBoxFlat] = [] -var _slot_sprites : Array[TextureRect] = [] -var _slot_bars : Array[ProgressBar] = [] -var _slot_fills : Array[StyleBoxFlat] = [] -var _signatures : Array = [] #evite de reappliquer l'affichage d'un emplacement qui n'a pas change +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() -#---------- actions d'entree ---------- - 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)]) @@ -77,73 +150,90 @@ func _build_ui() -> void: var column := VBoxContainer.new() column.mouse_filter = Control.MOUSE_FILTER_IGNORE - column.add_theme_constant_override("separation", 6) + column.add_theme_constant_override("separation", 8) _root_box.add_child(column) - _name_label = Label.new() - _name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE - _name_label.add_theme_font_size_override("font_size", 20) - _name_label.add_theme_constant_override("outline_size", 6) - _name_label.add_theme_color_override("font_outline_color", Color.BLACK) - column.add_child(_name_label) + _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", 8) - column.add_child(row) + row.add_theme_constant_override("separation", 4) + parent.add_child(row) _prev_icon = _make_hint_icon(prev_action) row.add_child(_prev_icon) - var slots_row := HBoxContainer.new() - slots_row.mouse_filter = Control.MOUSE_FILTER_IGNORE - slots_row.add_theme_constant_override("separation", 6) - row.add_child(slots_row) for i in PokemonTeam.MAX_SIZE: - _add_slot(slots_row) - _signatures.append(null) + 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 _add_slot(parent : Control) -> void: - var panel := PanelContainer.new() - panel.mouse_filter = Control.MOUSE_FILTER_IGNORE - panel.size_flags_vertical = Control.SIZE_SHRINK_END #les petits restent alignes en bas, le selectionne depasse vers le haut - var style := StyleBoxFlat.new() - style.bg_color = Color(0, 0, 0, 0.55) - style.set_corner_radius_all(10) - style.set_border_width_all(2) - style.set_content_margin_all(6) - panel.add_theme_stylebox_override("panel", style) - parent.add_child(panel) - - var box := VBoxContainer.new() - box.mouse_filter = Control.MOUSE_FILTER_IGNORE - box.add_theme_constant_override("separation", 3) - panel.add_child(box) - - var sprite := TextureRect.new() - sprite.mouse_filter = Control.MOUSE_FILTER_IGNORE - sprite.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - sprite.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - sprite.size_flags_vertical = Control.SIZE_EXPAND_FILL - box.add_child(sprite) - - var bar := ProgressBar.new() - bar.mouse_filter = Control.MOUSE_FILTER_IGNORE - bar.show_percentage = false - bar.custom_minimum_size = Vector2(0, 6) - var fill := StyleBoxFlat.new() - fill.bg_color = Color(0.3, 0.8, 0.35) - bar.add_theme_stylebox_override("fill", fill) - box.add_child(bar) - - _slot_panels.append(panel) - _slot_styles.append(style) - _slot_sprites.append(sprite) - _slot_bars.append(bar) - _slot_fills.append(fill) +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) @@ -152,7 +242,7 @@ func _make_hint_icon(action : StringName) -> TextureRect: var tex : Texture2D = ControllerIconTextureScript.new() tex.set("path", String(action)) tr.texture = tex - tr.custom_minimum_size = Vector2(32, 32) + 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 @@ -163,46 +253,68 @@ func _make_hint_icon(action : StringName) -> TextureRect: func _process(_delta : float) -> void: var team : PokemonTeam = GameData.player_team GameData.validate_selection() - var selected : int = GameData.selected_index + var selected_index : int = GameData.selected_index _root_box.visible = !team.is_empty() var able_count : int = 0 - for i in _slot_panels.size(): + for i in _balls.size(): var p : PokemonInstance = team.get_pokemon(i) - if p == null: - _slot_panels[i].visible = false - _signatures[i] = null - continue + if p != null and !p.is_fainted(): able_count += 1 - _slot_panels[i].visible = true - if !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(), p.current_hp, p.get_max_hp(), i == selected] - if _signatures[i] == sig: continue - _signatures[i] = sig - _apply_slot(i, p, i == selected) + 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) - #les icones de touches n'apparaissent que s'il y a un choix a faire - _prev_icon.visible = able_count > 1 - _next_icon.visible = able_count > 1 + 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 - var selected_pokemon : PokemonInstance = team.get_pokemon(selected) - var label_text : String = "" - if selected_pokemon: label_text = "%s Niv.%d" % [selected_pokemon.get_display_name(), selected_pokemon.level] - if _name_label.text != label_text: _name_label.text = label_text + _update_info(team.get_pokemon(selected_index)) -func _apply_slot(i : int, p : PokemonInstance, is_selected : bool) -> void: - var px : float = selected_slot_size if is_selected else slot_size - _slot_panels[i].custom_minimum_size = Vector2(px, px) - _slot_panels[i].modulate = Color(1.0, 0.55, 0.55) if p.is_fainted() else Color.WHITE - _slot_sprites[i].texture = p.species.get_menu_sprite(false) +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 - _slot_styles[i].border_color = Color(1.0, 0.9, 0.4) if is_selected else Color(1, 1, 1, 0.25) - _slot_styles[i].set_border_width_all(3 if is_selected else 2) + 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() - _slot_bars[i].max_value = max_hp - _slot_bars[i].value = p.current_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) - _slot_fills[i].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)) + _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))