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 var ability : AbilityData = null #talent (null = aucun) @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, p_ability : AbilityData = 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.ability = p_ability if p_ability != null else pick_default_ability(p_species) p.fill_moves_for_level() p.current_hp = p.get_max_hp() return p ## Talent par defaut d'une espece : tire au hasard parmi les talents normaux (le talent cache seulement s'il n'y en a pas d'autre). ## rng : generateur a utiliser pour un resultat reproductible (tests) ; sinon hasard global. static func pick_default_ability(p_species : PokemonData, rng : RandomNumberGenerator = null) -> AbilityData: var normal : Array[AbilityData] = [] var hidden : Array[AbilityData] = [] for entry in p_species.abilities: if entry == null or entry.ability == null: continue (hidden if entry.is_hidden else normal).append(entry.ability) var pool : Array[AbilityData] = normal if !normal.is_empty() else hidden if pool.is_empty(): return null return pool[rng.randi_range(0, pool.size() - 1)] if rng != null else pool.pick_random() #---------- 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 "", "ability": ability.api_name if ability 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 var ability_name : String = data.get("ability", "") p.ability = Pokedex.get_ability(ability_name) if ability_name != "" else pick_default_ability(species_data) p.current_hp = clampi(int(data.get("hp", p.get_max_hp())), 0, p.get_max_hp()) return p