Compare commits

...

4 commits

Author SHA1 Message Date
TheThomaas a0c32cba4d Add temp PCScreen 2026-10-04 14:20:46 +02:00
TheThomaas 8f0e053963 Handle Pokemon PC Boxes 2026-10-04 14:05:43 +02:00
TheThomaas 614e9c934a Save player team data 2026-10-04 13:59:28 +02:00
TheThomaas ca79aa060c Handle Pokemon instances and teams 2026-10-04 13:57:59 +02:00
10 changed files with 1090 additions and 0 deletions

View file

@ -2,13 +2,26 @@ extends Node
# Autoload : GameData # Autoload : GameData
var story_progress : int = 0 # 0 = début, 1 = après 1er badge, 2 = après 2e badge, etc. var story_progress : int = 0 # 0 = début, 1 = après 1er badge, 2 = après 2e badge, etc.
var player_team : PokemonTeam = PokemonTeam.new()
var player_storage : PokemonStorage = PokemonStorage.new()
func give_pokemon(pokemon : PokemonInstance) -> PokemonStorage.ReceiveResult:
#point d'entree unique pour obtenir un pokemon (starter, cadeau, capture...) :
#equipe si elle a de la place, sinon PC. A l'appelant d'informer le joueur selon le resultat.
return player_storage.receive_pokemon(player_team, pokemon)
func save() -> void: func save() -> void:
var config := ConfigFile.new() var config := ConfigFile.new()
config.set_value("progress", "story_progress", story_progress) config.set_value("progress", "story_progress", story_progress)
config.set_value("team", "data", player_team.to_dict())
config.set_value("storage", "data", player_storage.to_dict())
config.save("user://save.cfg") config.save("user://save.cfg")
func load_data() -> void: func load_data() -> void:
var config := ConfigFile.new() var config := ConfigFile.new()
if config.load("user://save.cfg") == OK: if config.load("user://save.cfg") == OK:
story_progress = config.get_value("progress", "story_progress", 0) story_progress = config.get_value("progress", "story_progress", 0)
var team_data : Variant = config.get_value("team", "data", {})
player_team = PokemonTeam.from_dict(team_data) if team_data is Dictionary else PokemonTeam.new()
var storage_data : Variant = config.get_value("storage", "data", {})
player_storage = PokemonStorage.from_dict(storage_data) if storage_data is Dictionary else PokemonStorage.new()

View file

@ -0,0 +1,25 @@
class_name MoveSlot
extends Resource
#une capacite equipee (active) d'un pokemon, avec ses PP actuels
@export var move : MoveData
@export var current_pp : int = 0
var max_pp : int:
get: return move.pp if move else 0
func _init(p_move : MoveData = null) -> void:
move = p_move
if move: current_pp = move.pp
func has_pp() -> bool:
return current_pp > 0
func use() -> bool:
if current_pp <= 0: return false
current_pp -= 1
return true
func restore() -> void:
current_pp = max_pp

View file

@ -0,0 +1,204 @@
class_name PokemonInstance
extends Resource
signal hp_changed(new_hp : int, max_hp : int)
signal fainted
enum Status { NONE, BURN, FREEZE, PARALYSIS, POISON, BAD_POISON, SLEEP }
const MAX_ACTIVE_MOVES : int = 4
const MAX_LEVEL : int = 100
@export var species : PokemonData
@export var nickname : String = ""
@export_range(1, 100) var level : int = 1
@export var nature : PokemonNature.Nature = PokemonNature.Nature.HARDY
@export var current_hp : int = 1
@export var status : Status = Status.NONE
@export_group("Capacités")
@export var learned_moves : Array[MoveData] = [] #toutes les capacites debloquees
@export var active_moves : Array[MoveSlot] = [] #les 4 equipees pour le combat (tirees de learned_moves)
@export_group("Monture")
@export var mount_data : MountProfile = null #null = ne peut pas etre monte
#---------- creation ----------
static func create(p_species : PokemonData, p_level : int, p_nature : int = -1, p_mount_data : MountProfile = null) -> PokemonInstance:
var p := PokemonInstance.new()
p.species = p_species
p.level = clampi(p_level, 1, MAX_LEVEL)
p.nature = (p_nature as PokemonNature.Nature) if p_nature >= 0 else PokemonNature.random_nature()
p.mount_data = p_mount_data
p.fill_moves_for_level()
p.current_hp = p.get_max_hp()
return p
#---------- infos ----------
func get_display_name() -> String:
if nickname != "": return nickname
return species.get_display_name()
func is_fainted() -> bool:
return current_hp <= 0
func can_be_mounted() -> bool:
return mount_data != null and !is_fainted()
#---------- stats ----------
#cles de PokemonData.base_stats (noms PokeAPI) -- A ADAPTER si tes cles sont differentes
const STAT_KEYS : Dictionary = {
PokemonNature.Stat.HP: "hp",
PokemonNature.Stat.ATTACK: "attack",
PokemonNature.Stat.DEFENSE: "defense",
PokemonNature.Stat.SP_ATTACK: "special-attack",
PokemonNature.Stat.SP_DEFENSE: "special-defense",
PokemonNature.Stat.SPEED: "speed",
}
func get_base_stat(stat : PokemonNature.Stat) -> int:
return int(species.base_stats.get(STAT_KEYS[stat], 0))
func get_max_hp() -> int:
#formule officielle avec IV = 0 et EV = 0
return floori(2.0 * get_base_stat(PokemonNature.Stat.HP) * level / 100.0) + level + 10
func get_stat(stat : PokemonNature.Stat) -> int:
if stat == PokemonNature.Stat.HP: return get_max_hp()
var raw : int = floori(2.0 * get_base_stat(stat) * level / 100.0) + 5
return floori(raw * PokemonNature.get_multiplier(nature, stat))
func set_level(new_level : int) -> void:
var old_max : int = get_max_hp()
level = clampi(new_level, 1, MAX_LEVEL)
#un pokemon K.O. le reste ; sinon il gagne la difference de PV max
if current_hp > 0:
current_hp = clampi(current_hp + get_max_hp() - old_max, 1, get_max_hp())
hp_changed.emit(current_hp, get_max_hp())
#---------- PV ----------
func take_damage(amount : int) -> void:
if is_fainted() or amount <= 0: return
current_hp = maxi(current_hp - amount, 0)
hp_changed.emit(current_hp, get_max_hp())
if current_hp == 0: fainted.emit()
func heal(amount : int) -> void:
if is_fainted() or amount <= 0: return #un K.O. se soigne avec revive()
current_hp = mini(current_hp + amount, get_max_hp())
hp_changed.emit(current_hp, get_max_hp())
func revive(hp_ratio : float = 0.5) -> void:
if !is_fainted(): return
current_hp = maxi(1, floori(get_max_hp() * hp_ratio))
status = Status.NONE
hp_changed.emit(current_hp, get_max_hp())
func heal_full() -> void:
current_hp = get_max_hp()
status = Status.NONE
for slot in active_moves: slot.restore()
hp_changed.emit(current_hp, get_max_hp())
#---------- capacites ----------
func learn_move(move : MoveData) -> void:
#debloque la capacite ; l'equipe automatiquement s'il reste une place
if move == null or learned_moves.has(move): return
learned_moves.append(move)
if active_moves.size() < MAX_ACTIVE_MOVES:
active_moves.append(MoveSlot.new(move))
func fill_moves_for_level() -> void:
#debloque toutes les capacites apprises par niveau jusqu'au niveau actuel,
#puis equipe les 4 dernieres (comme un pokemon sauvage ou un dresseur dans les jeux)
learned_moves.clear()
var entries : Array[LearnedMove] = []
for lm in species.learnset:
if lm.move != null and lm.method == "level-up" and lm.level <= level:
entries.append(lm)
entries.sort_custom(func(a : LearnedMove, b : LearnedMove) -> bool: return a.level < b.level)
for lm in entries:
if !learned_moves.has(lm.move): learned_moves.append(lm.move)
var picked : Array[MoveData] = []
for i in range(maxi(0, learned_moves.size() - MAX_ACTIVE_MOVES), learned_moves.size()):
picked.append(learned_moves[i])
set_active_moves(picked)
func set_active_moves(moves : Array[MoveData]) -> void:
#remplace les capacites equipees (max 4) ; les capacites inconnues sont debloquees au passage
active_moves.clear()
for m in moves:
if active_moves.size() >= MAX_ACTIVE_MOVES: break
if m == null or is_move_equipped(m): continue
if !learned_moves.has(m): learned_moves.append(m)
active_moves.append(MoveSlot.new(m))
func is_move_equipped(move : MoveData) -> bool:
for slot in active_moves:
if slot.move == move: return true
return false
func equip_move(move : MoveData, slot_index : int) -> bool:
#equipe une capacite deja debloquee dans un des 4 emplacements
if !learned_moves.has(move) or is_move_equipped(move): return false
if slot_index < 0 or slot_index >= MAX_ACTIVE_MOVES: return false
if slot_index >= active_moves.size():
active_moves.append(MoveSlot.new(move))
else:
active_moves[slot_index] = MoveSlot.new(move)
return true
#---------- sauvegarde ----------
#on ne stocke que des ids/noms d'API ; Pokedex retrouve les ressources au chargement
func to_dict() -> Dictionary:
var learned : Array = []
for m in learned_moves: learned.append(m.api_name)
var slots : Array = []
for s in active_moves: slots.append({"move": s.move.api_name, "pp": s.current_pp})
return {
"species": species.api_name,
"nickname": nickname,
"level": level,
"nature": int(nature),
"hp": current_hp,
"status": int(status),
"learned_moves": learned,
"active_moves": slots,
"mount_data": mount_data.resource_path if mount_data else "",
}
static func from_dict(data : Dictionary) -> PokemonInstance:
var species_data : PokemonData = Pokedex.get_pokemon(data.get("species", ""))
if species_data == null: return null
var p := PokemonInstance.new()
p.species = species_data
p.nickname = data.get("nickname", "")
p.level = clampi(int(data.get("level", 1)), 1, MAX_LEVEL)
p.nature = clampi(int(data.get("nature", 0)), 0, PokemonNature.Nature.size() - 1) as PokemonNature.Nature
p.status = clampi(int(data.get("status", 0)), 0, Status.size() - 1) as Status
for move_name in data.get("learned_moves", []):
var m : MoveData = Pokedex.get_move(str(move_name))
if m: p.learned_moves.append(m)
for slot_data in data.get("active_moves", []):
var m : MoveData = Pokedex.get_move(str(slot_data.get("move", "")))
if m == null: continue
var slot := MoveSlot.new(m)
slot.current_pp = clampi(int(slot_data.get("pp", m.pp)), 0, m.pp)
p.active_moves.append(slot)
#seuls les chemins res:// sont charges (jamais de ressource depuis user://)
var mount_path : String = data.get("mount_data", "")
if mount_path.begins_with("res://") and ResourceLoader.exists(mount_path):
p.mount_data = load(mount_path) as MountProfile
p.current_hp = clampi(int(data.get("hp", p.get_max_hp())), 0, p.get_max_hp())
return p

View file

@ -0,0 +1,28 @@
class_name PokemonNature
extends RefCounted
enum Stat { HP, ATTACK, DEFENSE, SP_ATTACK, SP_DEFENSE, SPEED }
#meme ordre que dans les jeux officiels (25 natures)
enum Nature {
HARDY, LONELY, BRAVE, ADAMANT, NAUGHTY,
BOLD, DOCILE, RELAXED, IMPISH, LAX,
TIMID, HASTY, SERIOUS, JOLLY, NAIVE,
MODEST, MILD, QUIET, BASHFUL, RASH,
CALM, GENTLE, SASSY, CAREFUL, QUIRKY,
}
#ordre des stats dans la table des natures : index // 5 = stat augmentee, index % 5 = stat diminuee
#(si les deux sont identiques, la nature est neutre)
const _STAT_ORDER : Array[int] = [Stat.ATTACK, Stat.DEFENSE, Stat.SPEED, Stat.SP_ATTACK, Stat.SP_DEFENSE]
static func get_multiplier(nature : Nature, stat : Stat) -> float:
var up_idx : int = floori(int(nature) / 5.0)
var down_idx : int = int(nature) % 5
if up_idx == down_idx: return 1.0
if stat == _STAT_ORDER[up_idx]: return 1.1
if stat == _STAT_ORDER[down_idx]: return 0.9
return 1.0
static func random_nature() -> Nature:
return Nature.values().pick_random() as Nature

View file

@ -0,0 +1,114 @@
class_name PokemonTeam
extends Resource
signal team_changed
signal pokemon_added(pokemon : PokemonInstance, index : int)
signal pokemon_removed(pokemon : PokemonInstance, index : int)
const MAX_SIZE : int = 6
@export var members : Array[PokemonInstance] = []
#---------- infos ----------
func size() -> int:
return members.size()
func is_empty() -> bool:
return members.is_empty()
func is_full() -> bool:
return members.size() >= MAX_SIZE
func get_pokemon(index : int) -> PokemonInstance:
return members[index] if _is_valid_index(index) else null
#---------- gestion ----------
func add_pokemon(pokemon : PokemonInstance) -> bool:
#renvoie false si l'equipe est pleine (l'appelant decide quoi en faire)
if pokemon == null or is_full() or members.has(pokemon): return false
members.append(pokemon)
pokemon_added.emit(pokemon, members.size() - 1)
team_changed.emit()
return true
func remove_at(index : int) -> PokemonInstance:
if !_is_valid_index(index): return null
var pokemon : PokemonInstance = members[index]
members.remove_at(index)
pokemon_removed.emit(pokemon, index)
team_changed.emit()
return pokemon
func swap(index_a : int, index_b : int) -> bool:
if index_a == index_b or !_is_valid_index(index_a) or !_is_valid_index(index_b): return false
var tmp : PokemonInstance = members[index_a]
members[index_a] = members[index_b]
members[index_b] = tmp
team_changed.emit()
return true
func replace_at(index : int, pokemon : PokemonInstance) -> PokemonInstance:
#remplace le pokemon a cet index et renvoie l'ancien (utilise pour les echanges avec le PC)
if pokemon == null or !_is_valid_index(index) or members.has(pokemon): return null
var old : PokemonInstance = members[index]
members[index] = pokemon
pokemon_removed.emit(old, index)
pokemon_added.emit(pokemon, index)
team_changed.emit()
return old
#---------- combat ----------
func get_first_able_index() -> int:
for i in members.size():
if !members[i].is_fainted(): return i
return -1
func get_first_able() -> PokemonInstance:
return get_pokemon(get_first_able_index())
func has_able_pokemon() -> bool:
return get_first_able_index() != -1
func get_able_pokemon() -> Array[PokemonInstance]:
var result : Array[PokemonInstance] = []
for p in members:
if !p.is_fainted(): result.append(p)
return result
func heal_all() -> void:
for p in members: p.heal_full()
#---------- montures ----------
func get_mountable() -> Array[PokemonInstance]:
var result : Array[PokemonInstance] = []
for p in members:
if p.can_be_mounted(): result.append(p)
return result
func has_mountable() -> bool:
for p in members:
if p.can_be_mounted(): return true
return false
#---------- interne ----------
func _is_valid_index(index : int) -> bool:
return index >= 0 and index < members.size()
#---------- sauvegarde ----------
func to_dict() -> Dictionary:
var list : Array = []
for p in members: list.append(p.to_dict())
return {"members": list}
static func from_dict(data : Dictionary) -> PokemonTeam:
var team := PokemonTeam.new()
for member_data in data.get("members", []):
var p := PokemonInstance.from_dict(member_data)
if p: team.add_pokemon(p)
return team

View file

@ -0,0 +1,21 @@
class_name TrainerData
extends Resource
#modele d'un dresseur PNJ : l'equipe reelle est fabriquee au debut du combat
@export var id : StringName
@export var names : Dictionary = {} # {"fr": "Pierre", "en": "Brock"}
@export var party : Array[TrainerPartyEntry] = []
func get_display_name() -> String:
return L10n.pick(names, String(id))
func create_team() -> PokemonTeam:
var team := PokemonTeam.new()
if party.size() > PokemonTeam.MAX_SIZE:
push_warning("TrainerData '%s' : %d pokémon dans le modèle, seuls les %d premiers seront utilisés." % [id, party.size(), PokemonTeam.MAX_SIZE])
for entry in party:
if entry == null: continue
var p := entry.create_instance()
if p and !team.add_pokemon(p): break #equipe pleine
return team

View file

@ -0,0 +1,25 @@
class_name TrainerPartyEntry
extends Resource
#un pokemon de l'equipe d'un dresseur PNJ (modele, pas un pokemon reel)
@export var species : PokemonData
@export_range(1, 100) var level : int = 5
@export var nickname : String = ""
@export_group("Nature")
@export var random_nature : bool = true
@export var nature : PokemonNature.Nature = PokemonNature.Nature.HARDY
@export_group("Capacités")
## Vide = capacites automatiques selon le niveau. Sinon : ces capacites (max 4) sont equipees.
@export var forced_moves : Array[MoveData] = []
func create_instance() -> PokemonInstance:
if species == null: return null
var nature_value : int = -1 if random_nature else int(nature)
var p := PokemonInstance.create(species, level, nature_value)
p.nickname = nickname
if !forced_moves.is_empty():
p.set_active_moves(forced_moves)
return p

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

View file

@ -0,0 +1,30 @@
class_name PokemonBox
extends Resource
#une boite du PC : un nombre fixe d'emplacements, chacun vide (null) ou occupe
var box_name : String = "" #vide = nom par defaut, a gerer par l'UI ("Boite 1"...)
var slots : Array[PokemonInstance] = []
func _init(p_size : int = 30) -> void:
slots.resize(p_size)
func size() -> int:
return slots.size()
func get_pokemon(slot : int) -> PokemonInstance:
return slots[slot] if slot >= 0 and slot < slots.size() else null
func first_free_slot() -> int:
for i in slots.size():
if slots[i] == null: return i
return -1
func is_full() -> bool:
return first_free_slot() == -1
func count() -> int:
var n : int = 0
for p in slots:
if p != null: n += 1
return n

View file

@ -0,0 +1,146 @@
class_name PokemonStorage
extends Resource
signal storage_changed
enum ReceiveResult { TEAM, BOX, FULL }
const NO_SLOT : Vector2i = Vector2i(-1, -1) #(boite, emplacement)
var boxes : Array[PokemonBox] = []
func _init(p_box_count : int = 8, p_box_size : int = 30) -> void:
for i in p_box_count:
boxes.append(PokemonBox.new(p_box_size))
#---------- infos ----------
func box_count() -> int:
return boxes.size()
func get_box(box_index : int) -> PokemonBox:
return boxes[box_index] if box_index >= 0 and box_index < boxes.size() else null
func get_pokemon(box_index : int, slot : int) -> PokemonInstance:
var box : PokemonBox = get_box(box_index)
return box.get_pokemon(slot) if box else null
func get_total_count() -> int:
var n : int = 0
for box in boxes: n += box.count()
return n
func is_full() -> bool:
return find_free_slot() == NO_SLOT
func find_free_slot() -> Vector2i:
for b in boxes.size():
var s : int = boxes[b].first_free_slot()
if s != -1: return Vector2i(b, s)
return NO_SLOT
#---------- reception d'un nouveau pokemon ----------
func receive_pokemon(team : PokemonTeam, pokemon : PokemonInstance) -> ReceiveResult:
#equipe d'abord ; si elle est pleine, premiere place libre du PC
if pokemon == null: return ReceiveResult.FULL
if team.add_pokemon(pokemon): return ReceiveResult.TEAM
var dest : Vector2i = find_free_slot()
if dest == NO_SLOT: return ReceiveResult.FULL
boxes[dest.x].slots[dest.y] = pokemon
storage_changed.emit()
return ReceiveResult.BOX
#---------- equipe <-> PC ----------
func deposit(team : PokemonTeam, team_index : int, box_index : int = -1, slot : int = -1) -> bool:
#equipe -> PC (box_index = -1 : premiere place libre)
if team.get_pokemon(team_index) == null: return false
if !_team_stays_valid(team, team_index, null): return false
var dest : Vector2i = find_free_slot() if box_index < 0 else Vector2i(box_index, slot)
if dest == NO_SLOT or get_box(dest.x) == null: return false
if dest.y < 0 or dest.y >= boxes[dest.x].size() or boxes[dest.x].slots[dest.y] != null: return false
var pokemon : PokemonInstance = team.remove_at(team_index)
boxes[dest.x].slots[dest.y] = pokemon
storage_changed.emit()
return true
func withdraw(team : PokemonTeam, box_index : int, slot : int) -> bool:
#PC -> equipe (a la fin) ; impossible si l'equipe est pleine
var pokemon : PokemonInstance = get_pokemon(box_index, slot)
if pokemon == null or team.is_full(): return false
if !team.add_pokemon(pokemon): return false
boxes[box_index].slots[slot] = null
storage_changed.emit()
return true
func swap_with_team(team : PokemonTeam, team_index : int, box_index : int, slot : int) -> bool:
#echange un pokemon de l'equipe avec celui d'un emplacement occupe du PC
var from_team : PokemonInstance = team.get_pokemon(team_index)
var from_box : PokemonInstance = get_pokemon(box_index, slot)
if from_team == null or from_box == null: return false
if !_team_stays_valid(team, team_index, from_box): return false
if team.replace_at(team_index, from_box) == null: return false
boxes[box_index].slots[slot] = from_team
storage_changed.emit()
return true
#---------- deplacements dans le PC ----------
func move_slot(box_a : int, slot_a : int, box_b : int, slot_b : int) -> bool:
#deplace (ou echange) le contenu d'un emplacement vers un autre, destination vide ou non
var source : PokemonInstance = get_pokemon(box_a, slot_a)
if source == null: return false
var dest_box : PokemonBox = get_box(box_b)
if dest_box == null or slot_b < 0 or slot_b >= dest_box.size(): return false
if box_a == box_b and slot_a == slot_b: return false
boxes[box_a].slots[slot_a] = dest_box.slots[slot_b]
dest_box.slots[slot_b] = source
storage_changed.emit()
return true
func release(box_index : int, slot : int) -> PokemonInstance:
#retire definitivement un pokemon du PC (relacher) ; renvoie le pokemon retire
var pokemon : PokemonInstance = get_pokemon(box_index, slot)
if pokemon == null: return null
boxes[box_index].slots[slot] = null
storage_changed.emit()
return pokemon
#---------- regle d'equipe ----------
func _team_stays_valid(team : PokemonTeam, removed_index : int, replacement : PokemonInstance) -> bool:
#l'equipe ne doit jamais etre vide, ni perdre son dernier pokemon apte
#(sauf si elle n'en avait deja aucun : tous K.O.)
var size_after : int = team.size() - (1 if replacement == null else 0)
if size_after < 1: return false
if !team.has_able_pokemon(): return true
for i in team.size():
if i != removed_index and !team.members[i].is_fainted(): return true
return replacement != null and !replacement.is_fainted()
#---------- sauvegarde ----------
func to_dict() -> Dictionary:
var boxes_data : Array = []
for box in boxes:
var slots_data : Array = []
for p in box.slots:
slots_data.append(p.to_dict() if p != null else null)
boxes_data.append({"name": box.box_name, "slots": slots_data})
return {"boxes": boxes_data}
static func from_dict(data : Dictionary) -> PokemonStorage:
var storage := PokemonStorage.new(0)
for box_data in data.get("boxes", []):
var slots_data : Array = box_data.get("slots", [])
var box := PokemonBox.new(slots_data.size())
box.box_name = box_data.get("name", "")
for i in slots_data.size():
if slots_data[i] is Dictionary:
box.slots[i] = PokemonInstance.from_dict(slots_data[i])
storage.boxes.append(box)
if storage.boxes.is_empty(): return PokemonStorage.new()
return storage