Add temp PCScreen

This commit is contained in:
TheThomaas 2026-10-04 14:20:46 +02:00
parent 8f0e053963
commit a0c32cba4d

484
scripts/storage/PCScreen.gd Normal file
View file

@ -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