Handle Pokemon instances and teams

This commit is contained in:
TheThomaas 2026-10-04 13:57:59 +02:00
parent 53bd8b701f
commit ca79aa060c
6 changed files with 407 additions and 0 deletions

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

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