321 lines
12 KiB
GDScript
321 lines
12 KiB
GDScript
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_and_offsets_preset(Control.PRESET_FULL_RECT) #le node est deja dans l'arbre : il faut aussi remettre les offsets a zero
|
|
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))
|