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