diff --git a/game/battle/battle.gd b/game/battle/battle.gd index 46004af..68f6fb0 100644 --- a/game/battle/battle.gd +++ b/game/battle/battle.gd @@ -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 diff --git a/game/battle/battle_ai.gd b/game/battle/battle_ai.gd index ec1a735..8d09d3e 100644 --- a/game/battle/battle_ai.gd +++ b/game/battle/battle_ai.gd @@ -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: diff --git a/game/battle/battle_debug.gd b/game/battle/battle_debug.gd index 74eaed9..8fe3369 100644 --- a/game/battle/battle_debug.gd +++ b/game/battle/battle_debug.gd @@ -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() \ No newline at end of file