Handle Pokemon instances and teams
This commit is contained in:
parent
53bd8b701f
commit
ca79aa060c
25
scripts/pokemon/move_slot.gd
Normal file
25
scripts/pokemon/move_slot.gd
Normal 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
|
||||
204
scripts/pokemon/pokemon_instance.gd
Normal file
204
scripts/pokemon/pokemon_instance.gd
Normal 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
|
||||
28
scripts/pokemon/pokemon_nature.gd
Normal file
28
scripts/pokemon/pokemon_nature.gd
Normal 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
|
||||
104
scripts/pokemon/pokemon_team.gd
Normal file
104
scripts/pokemon/pokemon_team.gd
Normal 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
|
||||
21
scripts/pokemon/trainer_data.gd
Normal file
21
scripts/pokemon/trainer_data.gd
Normal 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
|
||||
25
scripts/pokemon/trainer_party_entry.gd
Normal file
25
scripts/pokemon/trainer_party_entry.gd
Normal 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
|
||||
Loading…
Reference in a new issue