diff --git a/game/storage/team_screen.gd b/game/storage/team_screen.gd new file mode 100644 index 0000000..bd38c8d --- /dev/null +++ b/game/storage/team_screen.gd @@ -0,0 +1,271 @@ +class_name TeamScreen +extends Control + +#page "Equipe" : liste des 6 emplacements a gauche, fiche detaillee du pokemon a droite. +#manette d'abord (navigation par focus), clavier et souris fonctionnent aussi. +# +#usage : +# var screen := TeamScreen.new() +# canvas_layer.add_child(screen) +# screen.setup(GameData.player_team) +# screen.closed.connect(screen.queue_free) + +signal closed + +const ControllerIconTextureScript = preload("res://addons/controller_icons/objects/ControllerIconTexture.gd") + +const STAT_LINES : Array = [ + [PokemonNature.Stat.ATTACK, "Attaque"], + [PokemonNature.Stat.DEFENSE, "Défense"], + [PokemonNature.Stat.SP_ATTACK, "Att. Spé."], + [PokemonNature.Stat.SP_DEFENSE, "Déf. Spé."], + [PokemonNature.Stat.SPEED, "Vitesse"], +] + +var _team : PokemonTeam +var _slot_buttons : Array[Button] = [] +var _hp_bars : Array[ProgressBar] = [] +var _hp_fills : Array[StyleBoxFlat] = [] + +var _info_sprite : TextureRect +var _info_label : Label +var _empty_label : Label + +var _prev_mouse_mode : Input.MouseMode = Input.MOUSE_MODE_VISIBLE +var _mouse_saved : bool = false + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS #fonctionne meme si le jeu est en pause + _build_ui() + + _prev_mouse_mode = Input.mouse_mode + _mouse_saved = true + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + +func _exit_tree() -> void: + _restore_mouse() + +func setup(team : PokemonTeam) -> void: + if !is_node_ready(): await ready + _team = team + _team.team_changed.connect(_refresh) + _refresh() + _focus_first_member.call_deferred() + +func _unhandled_input(event : InputEvent) -> void: + if !visible: return + if event.is_action_pressed(&"ui_cancel"): + _close() + get_viewport().set_input_as_handled() + +#---------- construction de l'interface ---------- + +func _build_ui() -> void: + set_anchors_preset(Control.PRESET_FULL_RECT) + + var bg := ColorRect.new() + bg.color = Color(0, 0, 0, 0.8) + bg.set_anchors_preset(Control.PRESET_FULL_RECT) + add_child(bg) + + var margin := MarginContainer.new() + margin.set_anchors_preset(Control.PRESET_FULL_RECT) + for side in ["left", "top", "right", "bottom"]: + margin.add_theme_constant_override("margin_" + side, 40) + add_child(margin) + + var root := VBoxContainer.new() + root.size_flags_horizontal = Control.SIZE_EXPAND_FILL + root.add_theme_constant_override("separation", 12) + margin.add_child(root) + + var title := Label.new() + title.text = "Équipe" + title.add_theme_font_size_override("font_size", 28) + root.add_child(title) + + var columns := HBoxContainer.new() + columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL + columns.size_flags_vertical = Control.SIZE_EXPAND_FILL + columns.add_theme_constant_override("separation", 24) + root.add_child(columns) + + #liste des emplacements + var list_col := VBoxContainer.new() + list_col.add_theme_constant_override("separation", 6) + columns.add_child(list_col) + for i in PokemonTeam.MAX_SIZE: + _add_slot(list_col, i) + + #fiche detaillee + var info_panel := PanelContainer.new() + info_panel.custom_minimum_size = Vector2(520, 0) #un label avec retour a la ligne n'a pas de largeur minimale : on la garantit ici + info_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + columns.add_child(info_panel) + var info_margin := MarginContainer.new() + for side in ["left", "top", "right", "bottom"]: + info_margin.add_theme_constant_override("margin_" + side, 16) + info_panel.add_child(info_margin) + var info_box := HBoxContainer.new() + info_box.size_flags_horizontal = Control.SIZE_EXPAND_FILL + info_box.size_flags_vertical = Control.SIZE_EXPAND_FILL + info_box.add_theme_constant_override("separation", 20) + info_margin.add_child(info_box) + + _info_sprite = TextureRect.new() + _info_sprite.custom_minimum_size = Vector2(160, 160) + _info_sprite.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + _info_sprite.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + _info_sprite.size_flags_vertical = Control.SIZE_SHRINK_BEGIN + info_box.add_child(_info_sprite) + + _info_label = Label.new() + _info_label.custom_minimum_size = Vector2(300, 0) #evite que le texte s'ecrase a une lettre par ligne + _info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + info_box.add_child(_info_label) + + _empty_label = Label.new() + _empty_label.text = "Aucun Pokémon dans l'équipe pour le moment." + _empty_label.hide() + root.add_child(_empty_label) + + #aide des touches + var hints := HBoxContainer.new() + root.add_child(hints) + _make_hint(hints, &"ui_cancel", "Retour", _close) + +func _add_slot(parent : Control, index : int) -> void: + var b := Button.new() + b.custom_minimum_size = Vector2(380, 88) + b.alignment = HORIZONTAL_ALIGNMENT_LEFT + b.expand_icon = true + b.clip_text = true + b.add_theme_constant_override("icon_max_width", 64) + b.pressed.connect(_update_info.bind(index)) + b.focus_entered.connect(_update_info.bind(index)) + b.mouse_entered.connect(_on_slot_hovered.bind(index)) + parent.add_child(b) + _slot_buttons.append(b) + + #barre de PV en bas du bouton + var bar := ProgressBar.new() + bar.show_percentage = false + bar.custom_minimum_size = Vector2(0, 8) + bar.mouse_filter = Control.MOUSE_FILTER_IGNORE + bar.anchor_left = 0.0 + bar.anchor_right = 1.0 + bar.anchor_top = 1.0 + bar.anchor_bottom = 1.0 + bar.offset_left = 8.0 + bar.offset_right = -8.0 + bar.offset_top = -14.0 + bar.offset_bottom = -6.0 + var fill := StyleBoxFlat.new() + fill.bg_color = Color(0.3, 0.8, 0.35) + bar.add_theme_stylebox_override("fill", fill) + b.add_child(bar) + _hp_bars.append(bar) + _hp_fills.append(fill) + +func _make_hint(parent : Control, action : StringName, text : String, callback : Callable) -> Button: + #icone qui s'adapte au peripherique (addon controller_icons) + texte ; cliquable a la souris + var b := Button.new() + b.flat = true + b.focus_mode = Control.FOCUS_NONE + b.text = text + var tex : Texture2D = ControllerIconTextureScript.new() + tex.set("path", String(action)) + b.icon = tex + b.expand_icon = true + b.add_theme_constant_override("icon_max_width", 32) + b.pressed.connect(callback) + parent.add_child(b) + return b + +#---------- affichage ---------- + +func _refresh() -> void: + if _team == null: return + + for i in _slot_buttons.size(): + var b : Button = _slot_buttons[i] + var bar : ProgressBar = _hp_bars[i] + var p : PokemonInstance = _team.get_pokemon(i) + + if p == null: + b.icon = null + b.text = "—" + b.modulate = Color(1, 1, 1, 0.4) + b.focus_mode = Control.FOCUS_NONE + bar.hide() + continue + + var max_hp : int = p.get_max_hp() + b.focus_mode = Control.FOCUS_ALL + b.modulate = Color(1.0, 0.6, 0.6) if p.is_fainted() else Color.WHITE + b.icon = p.species.get_menu_sprite(false) + b.text = "%s Niv.%d\nPV %d/%d%s" % [p.get_display_name(), p.level, p.current_hp, max_hp, " (K.O.)" if p.is_fainted() else ""] + + bar.show() + bar.max_value = max_hp + bar.value = p.current_hp + var ratio : float = float(p.current_hp) / float(max_hp) + _hp_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)) + + _empty_label.visible = _team.is_empty() + + #garde la fiche a jour pour l'emplacement qui a le focus + var focused : int = _focused_index() + if focused != -1: _update_info(focused) + elif _team.is_empty(): _update_info(-1) + +func _update_info(index : int) -> void: + var p : PokemonInstance = _team.get_pokemon(index) if _team and index >= 0 else null + if p == null: + _info_sprite.texture = null + _info_label.text = "" + return + + _info_sprite.texture = p.species.get_menu_sprite(false) + var lines : Array[String] = [] + lines.append("%s Niv.%d" % [p.get_display_name(), p.level]) + lines.append("Nature : %s" % String(PokemonNature.Nature.keys()[p.nature]).capitalize()) + if p.can_be_mounted(): lines.append("Peut être monté") + lines.append("") + lines.append("PV : %d / %d" % [p.current_hp, p.get_max_hp()]) + for line in STAT_LINES: + lines.append("%s : %d" % [line[1], p.get_stat(line[0])]) + lines.append("") + lines.append("Capacités") + for slot in p.active_moves: + lines.append(" %s PP %d/%d" % [slot.move.get_display_name(), slot.current_pp, slot.max_pp]) + _info_label.text = "\n".join(lines) + +#---------- focus ---------- + +func _focused_index() -> int: + var focus_owner : Control = get_viewport().gui_get_focus_owner() + return _slot_buttons.find(focus_owner) if focus_owner is Button else -1 + +func _focus_first_member() -> void: + for b in _slot_buttons: + if b.focus_mode == Control.FOCUS_ALL: + b.grab_focus() + return + +func _on_slot_hovered(index : int) -> void: + if _slot_buttons[index].focus_mode == Control.FOCUS_ALL: + _slot_buttons[index].grab_focus() + +#---------- fermeture ---------- + +func _close() -> void: + _restore_mouse() + hide() + closed.emit() + +func _restore_mouse() -> void: + if !_mouse_saved: return + Input.mouse_mode = _prev_mouse_mode + _mouse_saved = false diff --git a/game/storage/team_screen.gd.uid b/game/storage/team_screen.gd.uid new file mode 100644 index 0000000..70a830c --- /dev/null +++ b/game/storage/team_screen.gd.uid @@ -0,0 +1 @@ +uid://bkncbi260q01o diff --git a/game/ui/pause_menu/pause_menu.gd b/game/ui/pause_menu/pause_menu.gd index fce3f89..f7e33f0 100644 --- a/game/ui/pause_menu/pause_menu.gd +++ b/game/ui/pause_menu/pause_menu.gd @@ -5,6 +5,7 @@ extends CanvasLayer @export var click_sound : AudioStream @onready var resume_button: Button = $PanelContainer/VBoxContainer/Resume +@onready var team_button: Button = $PanelContainer/VBoxContainer/Team @onready var storage_button: Button = $PanelContainer/VBoxContainer/Storage @onready var save_button: Button = $PanelContainer/VBoxContainer/Save @onready var load_button: Button = $PanelContainer/VBoxContainer/Load @@ -12,7 +13,8 @@ extends CanvasLayer @onready var animation_player: AnimationPlayer = $AnimationPlayer var is_transitioning : bool = false -var pc_screen : PCScreen = null +var sub_screen : Control = null #page ouverte depuis le menu (equipe, PC...) +var sub_screen_origin : Button = null #bouton qui a ouvert la page, pour lui rendre le focus var save_feedback_active : bool = false func _ready() -> void: @@ -21,7 +23,7 @@ func _ready() -> void: animation_player.play("RESET") func _unhandled_input(event: InputEvent) -> void: - if is_transitioning or pc_screen != null: return #le PC gere ses propres entrees + if is_transitioning or sub_screen != null: return #une page ouverte gere ses propres entrees if event.is_action_pressed("pause"): if get_tree().paused: resume() @@ -56,22 +58,37 @@ func resume() -> void: func _on_resume_pressed() -> void: resume() -func _on_storage_pressed() -> void: - if is_transitioning or pc_screen != null: return - UiAudio.play(click_sound) - - pc_screen = PCScreen.new() - add_child(pc_screen) #ajoute en dernier : s'affiche au-dessus du menu - pc_screen.setup(GameData.player_team, GameData.player_storage) - pc_screen.closed.connect(_on_pc_closed) - menu_panel.hide() #le flou de fond reste visible derriere le PC +#---------- pages (equipe, PC) ---------- -func _on_pc_closed() -> void: - if pc_screen: - pc_screen.queue_free() - pc_screen = null +func _open_sub_screen(screen : Control, origin : Button) -> void: + sub_screen = screen + sub_screen_origin = origin + add_child(screen) #ajoute en dernier : s'affiche au-dessus du menu + screen.connect("closed", _on_sub_screen_closed) + menu_panel.hide() #le flou de fond reste visible derriere la page + +func _on_sub_screen_closed() -> void: + if sub_screen: + sub_screen.queue_free() + sub_screen = null menu_panel.show() - storage_button.grab_focus() + if sub_screen_origin: sub_screen_origin.grab_focus() + +func _on_team_pressed() -> void: + if is_transitioning or sub_screen != null: return + UiAudio.play(click_sound) + var screen := TeamScreen.new() + _open_sub_screen(screen, team_button) + screen.setup(GameData.player_team) + +func _on_storage_pressed() -> void: + if is_transitioning or sub_screen != null: return + UiAudio.play(click_sound) + var screen := PCScreen.new() + _open_sub_screen(screen, storage_button) + screen.setup(GameData.player_team, GameData.player_storage) + +#---------- sauvegarde ---------- func _on_save_pressed() -> void: UiAudio.play(click_sound) diff --git a/game/ui/pause_menu/pause_menu.tscn b/game/ui/pause_menu/pause_menu.tscn index 5527ea0..c40685f 100644 --- a/game/ui/pause_menu/pause_menu.tscn +++ b/game/ui/pause_menu/pause_menu.tscn @@ -162,12 +162,18 @@ alignment = 1 [node name="Resume" type="Button" parent="PanelContainer/VBoxContainer" unique_id=1186461560] layout_mode = 2 -focus_neighbor_bottom = NodePath("../Storage") +focus_neighbor_bottom = NodePath("../Team") text = "Resume" +[node name="Team" type="Button" parent="PanelContainer/VBoxContainer" unique_id=1587203946] +layout_mode = 2 +focus_neighbor_top = NodePath("../Resume") +focus_neighbor_bottom = NodePath("../Storage") +text = "Pokémon Team" + [node name="Storage" type="Button" parent="PanelContainer/VBoxContainer" unique_id=1204857311] layout_mode = 2 -focus_neighbor_top = NodePath("../Resume") +focus_neighbor_top = NodePath("../Team") focus_neighbor_bottom = NodePath("../Save") text = "Pokémon Storage" @@ -198,6 +204,8 @@ text = "Quit" libraries/ = SubResource("AnimationLibrary_2nc7s") [connection signal="pressed" from="PanelContainer/VBoxContainer/Resume" to="." method="_on_resume_pressed"] +[connection signal="pressed" from="PanelContainer/VBoxContainer/Team" to="." method="_on_team_pressed"] +[connection signal="pressed" from="PanelContainer/VBoxContainer/Team" to="." method="_on_team_pressed"] [connection signal="pressed" from="PanelContainer/VBoxContainer/Storage" to="." method="_on_storage_pressed"] [connection signal="pressed" from="PanelContainer/VBoxContainer/Save" to="." method="_on_save_pressed"] [connection signal="pressed" from="PanelContainer/VBoxContainer/Load" to="." method="_on_load_pressed"]