From ca79aa060c38322bcf60ab09a65fef0632115119 Mon Sep 17 00:00:00 2001 From: TheThomaas Date: Sun, 4 Oct 2026 13:57:59 +0200 Subject: [PATCH] Handle Pokemon instances and teams --- scripts/pokemon/move_slot.gd | 25 +++ scripts/pokemon/pokemon_instance.gd | 204 +++++++++++++++++++++++++ scripts/pokemon/pokemon_nature.gd | 28 ++++ scripts/pokemon/pokemon_team.gd | 104 +++++++++++++ scripts/pokemon/trainer_data.gd | 21 +++ scripts/pokemon/trainer_party_entry.gd | 25 +++ 6 files changed, 407 insertions(+) create mode 100644 scripts/pokemon/move_slot.gd create mode 100644 scripts/pokemon/pokemon_instance.gd create mode 100644 scripts/pokemon/pokemon_nature.gd create mode 100644 scripts/pokemon/pokemon_team.gd create mode 100644 scripts/pokemon/trainer_data.gd create mode 100644 scripts/pokemon/trainer_party_entry.gd diff --git a/scripts/pokemon/move_slot.gd b/scripts/pokemon/move_slot.gd new file mode 100644 index 0000000..2132aad --- /dev/null +++ b/scripts/pokemon/move_slot.gd @@ -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 \ No newline at end of file diff --git a/scripts/pokemon/pokemon_instance.gd b/scripts/pokemon/pokemon_instance.gd new file mode 100644 index 0000000..7025569 --- /dev/null +++ b/scripts/pokemon/pokemon_instance.gd @@ -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 \ No newline at end of file diff --git a/scripts/pokemon/pokemon_nature.gd b/scripts/pokemon/pokemon_nature.gd new file mode 100644 index 0000000..a79958d --- /dev/null +++ b/scripts/pokemon/pokemon_nature.gd @@ -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 \ No newline at end of file diff --git a/scripts/pokemon/pokemon_team.gd b/scripts/pokemon/pokemon_team.gd new file mode 100644 index 0000000..89a14e2 --- /dev/null +++ b/scripts/pokemon/pokemon_team.gd @@ -0,0 +1,104 @@ +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 + +#---------- 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 \ No newline at end of file diff --git a/scripts/pokemon/trainer_data.gd b/scripts/pokemon/trainer_data.gd new file mode 100644 index 0000000..e4cf3b1 --- /dev/null +++ b/scripts/pokemon/trainer_data.gd @@ -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 \ No newline at end of file diff --git a/scripts/pokemon/trainer_party_entry.gd b/scripts/pokemon/trainer_party_entry.gd new file mode 100644 index 0000000..206cc3e --- /dev/null +++ b/scripts/pokemon/trainer_party_entry.gd @@ -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 \ No newline at end of file