486 lines
16 KiB
GDScript
486 lines
16 KiB
GDScript
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.ability != null: lines.append("Talent : %s" % p.ability.get_display_name())
|
|
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
|