From a0c32cba4d506ac29dcf55419379d3ed3c843c16 Mon Sep 17 00:00:00 2001 From: TheThomaas Date: Sun, 4 Oct 2026 14:20:46 +0200 Subject: [PATCH] Add temp PCScreen --- scripts/storage/PCScreen.gd | 484 ++++++++++++++++++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 scripts/storage/PCScreen.gd diff --git a/scripts/storage/PCScreen.gd b/scripts/storage/PCScreen.gd new file mode 100644 index 0000000..7e5081c --- /dev/null +++ b/scripts/storage/PCScreen.gd @@ -0,0 +1,484 @@ +class_name PCScreen +extends Control + +#interface du PC : equipe a gauche, boite courante au centre, fiche du pokemon a droite. +#manette d'abord (navigation par focus), clavier et souris fonctionnent aussi. +# +#usage : +# var pc := PCScreen.new() +# canvas_layer.add_child(pc) +# pc.setup(GameData.player_team, GameData.player_storage) +# pc.closed.connect(pc.queue_free) + +signal closed + +const ControllerIconTextureScript = preload("res://addons/controller_icons/objects/ControllerIconTexture.gd") + +const KIND_TEAM : int = 0 +const KIND_BOX : int = 1 +const NO_LOC : Vector3i = Vector3i(-1, -1, -1) #(type, index/boite, emplacement) +const GRID_COLUMNS : int = 6 + +#actions propres au PC : creees automatiquement si elles n'existent pas dans l'Input Map +@export var prev_box_action : StringName = &"pc_prev_box" #LB / PageUp +@export var next_box_action : StringName = &"pc_next_box" #RB / PageDown +@export var release_action : StringName = &"pc_release" #Y / Suppr + +var _team : PokemonTeam +var _storage : PokemonStorage +var _box_index : int = 0 + +var _held : PokemonInstance = null #pokemon "dans la main" (pris, pas encore pose) +var _held_loc : Vector3i = NO_LOC +var _focus_loc : Vector3i = NO_LOC +var _confirming : bool = false +var _release_loc : Vector3i = NO_LOC + +var _team_buttons : Array[Button] = [] +var _box_buttons : Array[Button] = [] + +var _grid : GridContainer +var _box_label : Label +var _info_sprite : TextureRect +var _info_label : Label +var _message_label : Label +var _message_tween : Tween +var _accept_hint : Button +var _cancel_hint : Button +var _confirm_overlay : CenterContainer +var _confirm_label : Label +var _confirm_no : Button +var _confirm_yes : Button + +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 + _ensure_actions() + _build_ui() + + #le curseur doit etre visible pour la souris (le jeu le capture peut-etre pour la camera) + _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, storage : PokemonStorage) -> void: + if !is_node_ready(): await ready + _team = team + _storage = storage + _team.team_changed.connect(_refresh) + _storage.storage_changed.connect(_refresh) + _rebuild_grid() + _refresh() + _team_buttons[0].grab_focus.call_deferred() + +#---------- actions d'entree ---------- + +func _ensure_actions() -> void: + _ensure_action(prev_box_action, [_joy_event(JOY_BUTTON_LEFT_SHOULDER), _key_event(KEY_PAGEUP)]) + _ensure_action(next_box_action, [_joy_event(JOY_BUTTON_RIGHT_SHOULDER), _key_event(KEY_PAGEDOWN)]) + _ensure_action(release_action, [_joy_event(JOY_BUTTON_Y), _key_event(KEY_DELETE)]) + +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: + if !visible or _team == null: return + + if _confirming: + if event.is_action_pressed(&"ui_cancel"): + _close_confirm() + get_viewport().set_input_as_handled() + return + + if event.is_action_pressed(&"ui_cancel"): _on_cancel() + elif event.is_action_pressed(prev_box_action): _change_box(-1) + elif event.is_action_pressed(next_box_action): _change_box(1) + elif event.is_action_pressed(release_action): _ask_release() + else: return + 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.add_theme_constant_override("separation", 12) + margin.add_child(root) + + var columns := HBoxContainer.new() + columns.size_flags_vertical = Control.SIZE_EXPAND_FILL + columns.add_theme_constant_override("separation", 24) + root.add_child(columns) + + #equipe + var team_col := VBoxContainer.new() + columns.add_child(team_col) + team_col.add_child(_make_title("Équipe")) + for i in PokemonTeam.MAX_SIZE: + var b := _make_slot_button(Vector2(300, 76)) + b.alignment = HORIZONTAL_ALIGNMENT_LEFT + b.pressed.connect(_on_team_slot_pressed.bind(i)) + b.focus_entered.connect(_on_team_slot_focused.bind(i)) + team_col.add_child(b) + _team_buttons.append(b) + + #boite + var box_col := VBoxContainer.new() + box_col.size_flags_horizontal = Control.SIZE_EXPAND_FILL + columns.add_child(box_col) + _box_label = _make_title("") + _box_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box_col.add_child(_box_label) + _grid = GridContainer.new() + _grid.columns = GRID_COLUMNS + box_col.add_child(_grid) + + #fiche + var info_panel := PanelContainer.new() + info_panel.custom_minimum_size = Vector2(280, 0) + columns.add_child(info_panel) + var info_box := VBoxContainer.new() + info_panel.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_box.add_child(_info_sprite) + _info_label = Label.new() + _info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + info_box.add_child(_info_label) + + #message temporaire + aide des touches + _message_label = Label.new() + _message_label.modulate.a = 0.0 + root.add_child(_message_label) + + var hints := HBoxContainer.new() + hints.add_theme_constant_override("separation", 24) + root.add_child(hints) + _accept_hint = _make_hint(hints, &"ui_accept", "Prendre") + _accept_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE #la souris clique directement sur les emplacements + _cancel_hint = _make_hint(hints, &"ui_cancel", "Fermer", _on_cancel) + _make_hint(hints, prev_box_action, "Boîte précédente", _change_box.bind(-1)) + _make_hint(hints, next_box_action, "Boîte suivante", _change_box.bind(1)) + _make_hint(hints, release_action, "Relâcher", _ask_release) + + #confirmation du relachement (focus sur "Non" par defaut) + _confirm_overlay = CenterContainer.new() + _confirm_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) + _confirm_overlay.mouse_filter = Control.MOUSE_FILTER_STOP + _confirm_overlay.hide() + add_child(_confirm_overlay) + var confirm_panel := PanelContainer.new() + _confirm_overlay.add_child(confirm_panel) + var confirm_box := VBoxContainer.new() + confirm_box.add_theme_constant_override("separation", 12) + confirm_panel.add_child(confirm_box) + _confirm_label = Label.new() + _confirm_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + confirm_box.add_child(_confirm_label) + var confirm_buttons := HBoxContainer.new() + confirm_buttons.alignment = BoxContainer.ALIGNMENT_CENTER + confirm_buttons.add_theme_constant_override("separation", 16) + confirm_box.add_child(confirm_buttons) + _confirm_no = Button.new() + _confirm_no.text = "Non" + _confirm_no.custom_minimum_size = Vector2(120, 44) + _confirm_no.pressed.connect(_close_confirm) + confirm_buttons.add_child(_confirm_no) + _confirm_yes = Button.new() + _confirm_yes.text = "Oui" + _confirm_yes.custom_minimum_size = Vector2(120, 44) + _confirm_yes.pressed.connect(_confirm_release) + confirm_buttons.add_child(_confirm_yes) + +func _make_title(text : String) -> Label: + var l := Label.new() + l.text = text + l.add_theme_font_size_override("font_size", 24) + return l + +func _make_slot_button(min_size : Vector2) -> Button: + var b := Button.new() + b.custom_minimum_size = min_size + b.expand_icon = true + b.clip_text = true + b.add_theme_constant_override("icon_max_width", int(min_size.y) - 12) + b.mouse_entered.connect(b.grab_focus) #la souris deplace le focus (la fiche suit le survol) + return b + +func _make_hint(parent : Control, action : StringName, text : String, callback : Callable = 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) + if callback.is_valid(): b.pressed.connect(callback) + parent.add_child(b) + return b + +func _rebuild_grid() -> void: + #recree les emplacements si la boite courante n'a pas la meme taille + var box : PokemonBox = _storage.get_box(_box_index) + if box == null or _box_buttons.size() == box.size(): return + + var focus_owner : Control = get_viewport().gui_get_focus_owner() + var had_focus : bool = focus_owner != null and _grid.is_ancestor_of(focus_owner) + + for b in _box_buttons: + _grid.remove_child(b) + b.queue_free() + _box_buttons.clear() + + for i in box.size(): + var b := _make_slot_button(Vector2(76, 76)) + b.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + b.pressed.connect(_on_box_slot_pressed.bind(i)) + b.focus_entered.connect(_on_box_slot_focused.bind(i)) + _grid.add_child(b) + _box_buttons.append(b) + + if had_focus and !_box_buttons.is_empty(): + _box_buttons[0].grab_focus.call_deferred() + +#---------- affichage ---------- + +func _refresh() -> void: + if _team == null: return + + for i in _team_buttons.size(): + _style_slot(_team_buttons[i], _team.get_pokemon(i), true) + + var box : PokemonBox = _storage.get_box(_box_index) + if box: + var title : String = box.box_name if box.box_name != "" else "Boîte %d" % (_box_index + 1) + _box_label.text = "%s (%d/%d)" % [title, box.count(), box.size()] + for i in _box_buttons.size(): + _style_slot(_box_buttons[i], box.get_pokemon(i), false) + + _accept_hint.text = "Poser" if _held else "Prendre" + _cancel_hint.text = "Annuler" if _held else "Fermer" + _update_info() + +func _style_slot(btn : Button, p : PokemonInstance, is_team : bool) -> void: + btn.modulate = Color.WHITE + if p == null: + btn.icon = null + btn.text = "Vide" if is_team else "" + btn.tooltip_text = "" + return + + btn.icon = p.species.get_menu_sprite(false) + btn.tooltip_text = "%s Niv.%d" % [p.get_display_name(), p.level] + if is_team: + btn.text = "%s Niv.%d\nPV %d/%d" % [p.get_display_name(), p.level, p.current_hp, p.get_max_hp()] + else: + btn.text = "" + + if p.is_fainted(): btn.modulate = Color(1.0, 0.55, 0.55) + if p == _held: btn.modulate = Color(btn.modulate, 0.35) #pris : on le voit "souleve" + +func _update_info() -> void: + var p : PokemonInstance = _get_at(_focus_loc) + 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("PV %d / %d" % [p.current_hp, p.get_max_hp()]) + lines.append("Nature : %s" % String(PokemonNature.Nature.keys()[p.nature]).capitalize()) + if p.can_be_mounted(): lines.append("Peut être monté") + lines.append("") + for slot in p.active_moves: + lines.append("%s %d/%d" % [slot.move.get_display_name(), slot.current_pp, slot.max_pp]) + _info_label.text = "\n".join(lines) + +func _show_message(text : String) -> void: + _message_label.text = text + _message_label.modulate.a = 1.0 + if _message_tween: _message_tween.kill() + _message_tween = create_tween() + _message_tween.tween_interval(2.5) + _message_tween.tween_property(_message_label, "modulate:a", 0.0, 0.5) + +#---------- emplacements ---------- + +func _get_at(loc : Vector3i) -> PokemonInstance: + if _team == null: return null + match loc.x: + KIND_TEAM: return _team.get_pokemon(loc.y) + KIND_BOX: return _storage.get_pokemon(loc.y, loc.z) + return null + +func _focus_button_for(loc : Vector3i) -> Button: + if loc.x == KIND_TEAM and loc.y >= 0 and loc.y < _team_buttons.size(): return _team_buttons[loc.y] + if loc.x == KIND_BOX and loc.y == _box_index and loc.z >= 0 and loc.z < _box_buttons.size(): return _box_buttons[loc.z] + return null + +func _set_slots_focusable(on : bool) -> void: + var mode : Control.FocusMode = Control.FOCUS_ALL if on else Control.FOCUS_NONE + for b in _team_buttons: b.focus_mode = mode + for b in _box_buttons: b.focus_mode = mode + +func _on_team_slot_focused(i : int) -> void: + _focus_loc = Vector3i(KIND_TEAM, i, 0) + _update_info() + +func _on_box_slot_focused(i : int) -> void: + _focus_loc = Vector3i(KIND_BOX, _box_index, i) + _update_info() + +func _on_team_slot_pressed(i : int) -> void: + _on_slot_pressed(Vector3i(KIND_TEAM, i, 0)) + +func _on_box_slot_pressed(i : int) -> void: + _on_slot_pressed(Vector3i(KIND_BOX, _box_index, i)) + +#---------- prendre / poser ---------- + +func _on_slot_pressed(loc : Vector3i) -> void: + if _confirming: return + var target : PokemonInstance = _get_at(loc) + + if _held == null: + if target == null: return + _held = target + _held_loc = loc + _refresh() + return + + if loc == _held_loc: + _cancel_hold() + return + + if _attempt_move(_held_loc, loc): + _held = null + _held_loc = NO_LOC + _refresh() + +func _cancel_hold() -> void: + _held = null + _held_loc = NO_LOC + _refresh() + +func _attempt_move(from : Vector3i, to : Vector3i) -> bool: + var ok : bool = false + if from.x == KIND_TEAM and to.x == KIND_TEAM: + if _team.get_pokemon(to.y) == null: + _show_message("Choisis un Pokémon de l'équipe avec qui échanger.") + return false + ok = _team.swap(from.y, to.y) + elif from.x == KIND_TEAM and to.x == KIND_BOX: + if _storage.get_pokemon(to.y, to.z) == null: + ok = _storage.deposit(_team, from.y, to.y, to.z) + else: + ok = _storage.swap_with_team(_team, from.y, to.y, to.z) + elif from.x == KIND_BOX and to.x == KIND_TEAM: + if _team.get_pokemon(to.y) == null: + ok = _storage.withdraw(_team, from.y, from.z) + else: + ok = _storage.swap_with_team(_team, to.y, from.y, from.z) + else: + ok = _storage.move_slot(from.y, from.z, to.y, to.z) + + if !ok: + if from.x == KIND_TEAM or to.x == KIND_TEAM: + _show_message("Impossible : l'équipe doit garder au moins un Pokémon apte au combat.") + else: + _show_message("Déplacement impossible.") + return ok + +#---------- boites ---------- + +func _change_box(direction : int) -> void: + if _confirming or _storage == null or _storage.box_count() == 0: return + _box_index = posmod(_box_index + direction, _storage.box_count()) + _rebuild_grid() + #si le focus est sur la grille, _focus_loc doit suivre la nouvelle boite + if _focus_loc.x == KIND_BOX: _focus_loc = Vector3i(KIND_BOX, _box_index, _focus_loc.z) + _refresh() + +#---------- relacher ---------- + +func _ask_release() -> void: + if _confirming or _held != null: return + var p : PokemonInstance = _get_at(_focus_loc) + if p == null: return + if _focus_loc.x == KIND_TEAM: + _show_message("Dépose-le d'abord dans le PC pour le relâcher.") + return + + _release_loc = _focus_loc + _confirm_label.text = "Relâcher %s ?\nIl ne reviendra pas." % p.get_display_name() + _confirming = true + _set_slots_focusable(false) + _confirm_overlay.show() + _confirm_no.grab_focus() + +func _confirm_release() -> void: + var released : PokemonInstance = _storage.release(_release_loc.y, _release_loc.z) + _close_confirm() + if released: _show_message("%s a été relâché." % released.get_display_name()) + +func _close_confirm() -> void: + _confirming = false + _confirm_overlay.hide() + _set_slots_focusable(true) + var b : Button = _focus_button_for(_release_loc) + if b == null: b = _team_buttons[0] + b.grab_focus.call_deferred() + +#---------- fermeture ---------- + +func _on_cancel() -> void: + if _held != null: + _cancel_hold() + else: + _close() + +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 \ No newline at end of file