Update battle system

This commit is contained in:
TheThomaas 2026-10-07 17:58:38 +02:00
parent f2013efadc
commit f4cc553f35
3 changed files with 86 additions and 6 deletions

View file

@ -12,7 +12,7 @@ extends RefCounted
signal event_emitted(event : BattleEvent)
enum Outcome { NONE, PLAYER_WON, PLAYER_LOST, FLED }
enum Outcome { NONE, PLAYER_WON, PLAYER_LOST, FLED, DRAW }
const PLAYER_SIDE : int = 0
@ -22,6 +22,7 @@ var setup : BattleSetup
var presenter : BattlePresenter
var rng : RandomNumberGenerator = RandomNumberGenerator.new()
var turn : int = 0
var max_turns : int = 0 #0 = illimite ; sinon le combat s'arrete en match nul apres ce nombre de tours (utile pour les tests)
var outcome : Outcome = Outcome.NONE
var slots : Array[BattleSlot] = []
@ -56,6 +57,9 @@ func run() -> Outcome:
return await _finish()
while outcome == Outcome.NONE:
if max_turns > 0 and turn >= max_turns:
outcome = Outcome.DRAW
break
turn += 1
var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START)
turn_event.amount = turn
@ -79,6 +83,7 @@ func _finish() -> Outcome:
Outcome.PLAYER_WON: end_event.text = "Victoire !"
Outcome.PLAYER_LOST: end_event.text = "Défaite..."
Outcome.FLED: end_event.text = "Vous avez pris la fuite."
Outcome.DRAW: end_event.text = "Match nul (limite de tours atteinte)."
await _emit(end_event)
return outcome

View file

@ -15,11 +15,13 @@ var hp_threshold : float = 0.4 #ne change que si ses PV sont sous ce ratio
var matchup_threshold : float = -1.0 #ne change que si le rapport de types est pire que cette valeur
var min_improvement : float = 1.0 #le remplacant doit etre meilleur d'au moins cette valeur
var min_replacement_hp : float = 0.3 #ne fait pas entrer un pokemon quasi K.O.
var switch_cooldown : int = 2 #tours minimum entre deux changements volontaires d'un meme emplacement
const KILL_SCORE : float = 20.0 #bonus de score d'une attaque qui acheve un adversaire (cf. _score)
var _claimed : Array[PokemonInstance] = [] #remplacants deja choisis ce tour (evite 2 emplacements sur le meme)
var _claimed_turn : int = -1
var _last_switch : Dictionary = {} #id d'emplacement -> dernier tour ou il a change volontairement
func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction:
if _claimed_turn != battle.turn:
@ -71,9 +73,13 @@ func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance:
func _consider_switch(battle : Battle, slot : BattleSlot, best_attack_score : float, no_usable_move : bool) -> BattleAction:
if !can_switch: return null
#remplacants possibles : pas deja choisis ce tour ; et, si le pokemon actuel n'a plus de capacite,
#changer n'a de sens que si le remplacant en a une (sinon on tournerait en rond sans jamais attaquer)
var bench : Array[PokemonInstance] = []
for p in battle.get_replacement_options(slot):
if !_claimed.has(p): bench.append(p)
if _claimed.has(p): continue
if no_usable_move and !_has_usable_move(p): continue
bench.append(p)
if bench.is_empty(): return null
var opponents : Array[BattleSlot] = battle.get_opponents(slot)
@ -83,6 +89,7 @@ func _consider_switch(battle : Battle, slot : BattleSlot, best_attack_score : fl
if best_attack_score >= KILL_SCORE: return null #peut achever un adversaire : on attaque
var hp_ratio : float = float(slot.mon.pokemon.current_hp) / float(slot.mon.pokemon.get_max_hp())
if hp_ratio > hp_threshold or current > matchup_threshold: return null
if battle.turn - int(_last_switch.get(slot.id, -999)) < switch_cooldown: return null
if battle.rng.randf() > switch_chance: return null
var best : PokemonInstance = null
@ -97,6 +104,7 @@ func _consider_switch(battle : Battle, slot : BattleSlot, best_attack_score : fl
if !no_usable_move and best_value < current + min_improvement: return null
_claimed.append(best)
_last_switch[slot.id] = battle.turn
return BattleAction.switch_pokemon(slot, best)
func _matchup(my_types : Array[String], opponents : Array[BattleSlot]) -> float:
@ -115,6 +123,11 @@ func _matchup(my_types : Array[String], opponents : Array[BattleSlot]) -> float:
total += offense - defense
return total / opponents.size()
func _has_usable_move(pokemon : PokemonInstance) -> bool:
for ms in pokemon.active_moves:
if ms.has_pp(): return true
return false
func _types_of(pokemon : PokemonInstance) -> Array[String]:
var result : Array[String] = []
for t in pokemon.species.types:

View file

@ -1,7 +1,9 @@
class_name BattleDebug
extends Node
#banc d'essai : simule un combat complet IA contre IA et ecrit tout dans la console.
#banc d'essai : simule un combat complet IA contre IA.
#le journal n'est PAS ecrit dans la console (trop lent) : il est garde en memoire, puis
#affiche a l'ecran et/ou ecrit dans un fichier a la fin du combat. La console ne recoit qu'un resume.
#ajoute ce node a une scene, regle le format dans l'inspecteur, lance la scene.
#utilise des equipes aleatoires (par defaut) : n'abime pas ton equipe de jeu.
@ -12,6 +14,14 @@ extends Node
@export_range(1, 6) var team_size : int = 3
@export_range(1, 100) var level : int = 10
@export var use_game_team : bool = false #true : utilise GameData.player_team (qui sera modifiee !)
@export var max_turns : int = 100 #securite : match nul au-dela (0 = illimite)
@export_group("Sortie")
@export var show_log_on_screen : bool = true
@export var write_log_file : bool = true
const LOG_PATH : String = "user://battle_debug.log"
var _log_layer : CanvasLayer = null
func _ready() -> void:
if run_on_ready:
@ -35,10 +45,18 @@ func run_test() -> void:
push_error("BattleDebug : combat invalide (une equipe n'a aucun pokemon apte)")
return
print("===== Combat : %d contre %d =====" % [player_slots, enemy_count * enemy_slots_each])
var battle := Battle.new(setup, BattlePrintPresenter.new())
var presenter := BattleLogPresenter.new()
var battle := Battle.new(setup, presenter)
battle.max_turns = max_turns
var outcome : Battle.Outcome = await battle.run()
print("===== Resultat : %s apres %d tours =====" % [Battle.Outcome.keys()[outcome], battle.turn])
var header : String = "===== Combat : %d contre %d =====" % [player_slots, enemy_count * enemy_slots_each]
var summary : String = "Résultat : %s après %d tours (%d lignes de journal)" % [Battle.Outcome.keys()[outcome], battle.turn, presenter.lines.size()]
var text : String = header + "\n" + "\n".join(presenter.lines) + "\n===== " + summary + " ====="
print("[BattleDebug] " + summary) #une seule ligne dans la console
if write_log_file: _write_log(text)
if show_log_on_screen: _show_log(text, summary)
func _random_team(size : int) -> PokemonTeam:
var team := PokemonTeam.new()
@ -50,3 +68,47 @@ func _random_team(size : int) -> PokemonTeam:
var species : PokemonData = Pokedex.get_pokemon(ids.pick_random())
if species: team.add_pokemon(PokemonInstance.create(species, level))
return team
#---------- sortie ----------
func _write_log(text : String) -> void:
var file := FileAccess.open(LOG_PATH, FileAccess.WRITE)
if file == null:
push_warning("BattleDebug : impossible d'ecrire le journal (%s)" % LOG_PATH)
return
file.store_string(text)
file.close()
print("[BattleDebug] Journal ecrit dans : " + ProjectSettings.globalize_path(LOG_PATH))
func _show_log(text : String, summary : String) -> void:
#panneau plein ecran avec le journal defilant, ferme par un bouton (ou B / Echap)
if _log_layer != null: _log_layer.queue_free()
_log_layer = CanvasLayer.new()
_log_layer.layer = 100
add_child(_log_layer)
var panel := PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
_log_layer.add_child(panel)
var box := VBoxContainer.new()
panel.add_child(box)
var title := Label.new()
title.text = summary
box.add_child(title)
var log_label := RichTextLabel.new()
log_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
log_label.selection_enabled = true
log_label.scroll_active = true
log_label.text = text
box.add_child(log_label)
var close := Button.new()
close.text = "Fermer"
close.pressed.connect(func() -> void:
_log_layer.queue_free()
_log_layer = null)
box.add_child(close)
close.grab_focus()