diff --git a/game/battle/battle.gd b/game/battle/battle.gd index ed4b95f..ec6db23 100644 --- a/game/battle/battle.gd +++ b/game/battle/battle.gd @@ -9,6 +9,9 @@ extends RefCounted # var outcome : Battle.Outcome = await battle.run() # #a chaque evenement, la presentation est appelee et le combat attend qu'elle ait fini. +# +#les effets (statuts, confusion...) se branchent par des crochets : voir BattleEffect. +#les capacites sont executees par MoveExecutor. signal event_emitted(event : BattleEvent) @@ -49,11 +52,11 @@ func _build_slots() -> void: #---------- boucle principale ---------- func run() -> Outcome: - await _emit(BattleEvent.make(BattleEvent.Type.BATTLE_START)) + await emit_event(BattleEvent.make(BattleEvent.Type.BATTLE_START)) for slot in slots: - await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) + await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) - if _check_end(): + if check_end(): return await _finish() while outcome == Outcome.NONE: @@ -63,17 +66,22 @@ func run() -> Outcome: turn += 1 var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START) turn_event.amount = turn - await _emit(turn_event) + await emit_event(turn_event) + for slot in slots: + if slot.mon != null: slot.mon.acted_this_turn = false var actions : Array[BattleAction] = await _collect_actions() _sort_actions(actions) for action in actions: await _execute(action) - if _check_end(): break + if check_end(): break + if outcome != Outcome.NONE: break + + await _end_of_turn() if outcome != Outcome.NONE: break await _replace_fainted() - if _check_end(): break + if check_end(): break return await _finish() @@ -84,18 +92,120 @@ func _finish() -> Outcome: 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) + await emit_event(end_event) return outcome -func _emit(event : BattleEvent) -> void: +#---------- evenements et aides pour les effets ---------- + +func emit_event(event : BattleEvent) -> void: event_emitted.emit(event) if presenter != null: await presenter.present(event) -func _msg(text : String) -> void: +## Affiche un message. +func message(text : String) -> void: var e := BattleEvent.make(BattleEvent.Type.MESSAGE) e.text = text - await _emit(e) + await emit_event(e) + +## Inflige des degats et renvoie ceux reellement subis (limites aux PV restants). Emet l'evenement DAMAGE. +func deal_damage(source : BattleSlot, target : BattleSlot, amount : int) -> int: + var before : int = target.mon.pokemon.current_hp + target.mon.pokemon.take_damage(amount) + var event := BattleEvent.make(BattleEvent.Type.DAMAGE, source, target) + event.amount = before - target.mon.pokemon.current_hp + event.hp = target.mon.pokemon.current_hp + event.max_hp = target.mon.pokemon.get_max_hp() + await emit_event(event) + return event.amount + +## Degats "de l'environnement" (brulure, poison, contrecoup...) : message, degats, puis K.O. eventuel. +func hurt(slot : BattleSlot, amount : int, text : String = "") -> void: + if !slot.is_active(): return + if text != "": await message(text) + await deal_damage(slot, slot, amount) + if slot.mon.is_fainted(): + await emit_event(BattleEvent.make(BattleEvent.Type.FAINTED, slot)) + +## Soigne un pokemon et renvoie les PV reellement rendus (0 = deja au maximum, aucun evenement). +func heal(slot : BattleSlot, amount : int, text : String = "") -> int: + if !slot.is_active() or amount <= 0: return 0 + var pokemon : PokemonInstance = slot.mon.pokemon + var before : int = pokemon.current_hp + pokemon.heal(amount) + var healed : int = pokemon.current_hp - before + if healed > 0: + var event := BattleEvent.make(BattleEvent.Type.HEAL, null, slot) + event.amount = healed + event.hp = pokemon.current_hp + event.max_hp = pokemon.get_max_hp() + event.text = text + await emit_event(event) + return healed + +## Tente d'infliger un statut (nom PokeAPI : "burn", "paralysis", "confusion"...). Renvoie true si applique. +## announce_failure : afficher pourquoi ca echoue (capacite de statut), sinon echec silencieux (effet secondaire). +func inflict_status(source : BattleSlot, target : BattleSlot, ailment : String, move : MoveData, announce_failure : bool) -> bool: + if !target.is_active(): return false + var mon : BattleMon = target.mon + var pokemon_name : String = mon.display_name() + + if ailment == "confusion": + if mon.has_volatile(&"confusion"): + if announce_failure: await message("%s est déjà confus !" % pokemon_name) + return false + mon.add_volatile(ConfusionEffect.new(rng.randi_range(2, 5))) + await message("%s est confus !" % pokemon_name) + return true + + var status : int = StatusRegistry.from_ailment(ailment, move) + if status == -1: + if announce_failure: await message("Mais cela n'a pas d'effet pour l'instant.") + return false + if mon.pokemon.status != PokemonInstance.Status.NONE: + if announce_failure: await message("Mais cela échoue !") + return false + if StatusRegistry.is_immune(mon.get_types(), status): + if announce_failure: await emit_event(BattleEvent.make(BattleEvent.Type.NO_EFFECT, source, target)) + return false + + mon.pokemon.status = status as PokemonInstance.Status + mon.on_status_applied(status, rng) + var event := BattleEvent.make(BattleEvent.Type.STATUS_CHANGED, null, target) + event.amount = status + event.text = StatusRegistry.apply_message(status, pokemon_name) + await emit_event(event) + return true + +## Guerit le statut principal d'un pokemon (reveil, degel...). +func cure_status(slot : BattleSlot, text : String = "") -> void: + var mon : BattleMon = slot.mon + mon.pokemon.status = PokemonInstance.Status.NONE + mon.status_turns = 0 + mon.toxic_counter = 0 + var event := BattleEvent.make(BattleEvent.Type.STATUS_CHANGED, null, slot) + event.amount = 0 + event.text = text + await emit_event(event) + +## Change un boost de stat (nom PokeAPI : "attack", "speed", "accuracy"...) et affiche le message. +func change_stat(slot : BattleSlot, stat_name : String, delta : int) -> void: + if !slot.is_active() or delta == 0: return + var applied : int = slot.mon.change_stage(stat_name, delta) + var label : String = BattleMon.STAT_LABELS.get(stat_name, stat_name) + var pokemon_name : String = slot.mon.display_name() + if applied == 0: + await message("%s de %s ne peut plus %s !" % [label, pokemon_name, "monter" if delta > 0 else "baisser"]) + return + var degree : String = "" if absi(applied) == 1 else (" beaucoup" if absi(applied) == 2 else " énormément") + await message("%s de %s %s%s !" % [label, pokemon_name, "monte" if applied > 0 else "baisse", degree]) + +## Crochet "avant l'action" : false si un effet empeche le pokemon d'agir (sommeil, peur...). +func run_before_action(action : BattleAction) -> bool: + for effect in action.slot.mon.get_effects(): + if !action.slot.is_active(): return false + if !await effect.before_action(self, action.slot, action): return false + return action.slot.is_active() #---------- fin de combat ---------- @@ -104,7 +214,7 @@ func _side_alive(side : int) -> bool: if participant.side == side and participant.has_able_pokemon(): return true return false -func _check_end() -> bool: +func check_end() -> bool: if outcome != Outcome.NONE: return true var player_alive : bool = _side_alive(PLAYER_SIDE) var enemy_alive : bool = _side_alive(1 - PLAYER_SIDE) @@ -140,94 +250,44 @@ func _sort_actions(actions : Array[BattleAction]) -> void: func _execute(action : BattleAction) -> void: if !action.slot.is_active(): return #mis K.O. avant son tour + action.slot.mon.acted_this_turn = true match action.type: - BattleAction.Type.MOVE: await _execute_move(action) + BattleAction.Type.MOVE: await MoveExecutor.execute(self, action) BattleAction.Type.SWITCH: await _execute_switch(action) BattleAction.Type.FLEE: await _execute_flee(action) func _execute_flee(action : BattleAction) -> void: if setup.can_flee and action.slot.side == PLAYER_SIDE: outcome = Outcome.FLED - await _emit(BattleEvent.make(BattleEvent.Type.FLED, action.slot)) + await emit_event(BattleEvent.make(BattleEvent.Type.FLED, action.slot)) else: - await _msg("Impossible de fuir !") + await message("Impossible de fuir !") func _execute_switch(action : BattleAction) -> void: var slot : BattleSlot = action.slot if !get_replacement_options(slot).has(action.switch_to): return - await _emit(BattleEvent.make(BattleEvent.Type.WITHDRAWN, slot)) + await emit_event(BattleEvent.make(BattleEvent.Type.WITHDRAWN, slot)) slot.set_pokemon(action.switch_to) - await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) + slot.mon.acted_this_turn = true + await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) -func _execute_move(action : BattleAction) -> void: - var actor : BattleSlot = action.slot - var move : MoveData = action.move - - if action.move_slot != null: - if !action.move_slot.use(): - await _msg("%s n'a plus de PP pour cette capacité !" % actor.mon.display_name()) - return - - var used := BattleEvent.make(BattleEvent.Type.MOVE_USED, actor) - used.move = move - await _emit(used) - - var targets : Array[BattleSlot] = TargetResolver.resolve(self, actor, move, action.chosen_target) - if targets.is_empty(): - await _msg("Mais cela échoue !") - return - - var spread : bool = targets.size() > 1 - var status_message_shown : bool = false - - for target in targets: - if !target.is_active(): continue - - if move.power <= 0 or move.category == "status": - #les effets des capacites de statut ne sont pas encore implementes - if !status_message_shown: - status_message_shown = true - await _msg("Mais cela n'a pas d'effet pour l'instant.") - continue - - if !_hits(move): - await _emit(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target)) - continue - - var calc : Dictionary = DamageCalculator.calculate(actor.mon, target.mon, move, rng, spread) - var effectiveness : float = calc["effectiveness"] - if effectiveness == 0.0: - await _emit(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target)) - continue - - await _apply_damage(actor, target, int(calc["damage"])) - if calc["critical"]: await _emit(BattleEvent.make(BattleEvent.Type.CRITICAL, actor, target)) - if effectiveness > 1.0: await _emit(BattleEvent.make(BattleEvent.Type.SUPER_EFFECTIVE, actor, target)) - elif effectiveness < 1.0: await _emit(BattleEvent.make(BattleEvent.Type.NOT_VERY_EFFECTIVE, actor, target)) - if target.mon.is_fainted(): await _emit(BattleEvent.make(BattleEvent.Type.FAINTED, target)) - - if _check_end(): return - - #Lutte : contrecoup pour l'utilisateur - if move.api_name == "struggle" and actor.mon != null and !actor.mon.is_fainted(): - var recoil : int = maxi(1, floori(actor.mon.pokemon.get_max_hp() / 4.0)) - await _apply_damage(actor, actor, recoil) - if actor.mon.is_fainted(): await _emit(BattleEvent.make(BattleEvent.Type.FAINTED, actor)) +#---------- fin de tour ---------- -func _apply_damage(source : BattleSlot, target : BattleSlot, amount : int) -> void: - var before : int = target.mon.pokemon.current_hp - target.mon.pokemon.take_damage(amount) - var event := BattleEvent.make(BattleEvent.Type.DAMAGE, source, target) - event.amount = before - target.mon.pokemon.current_hp - event.hp = target.mon.pokemon.current_hp - event.max_hp = target.mon.pokemon.get_max_hp() - await _emit(event) - -func _hits(move : MoveData) -> bool: - #precision 0 = ne rate jamais (valeur PokeAPI "null") - return move.accuracy <= 0 or rng.randi_range(1, 100) <= move.accuracy - -#---------- remplacements ---------- +func _end_of_turn() -> void: + #effets de fin de tour (brulure, poison...), du plus rapide au plus lent + var ordered : Array[BattleSlot] = get_active_slots() + ordered.sort_custom(func(a : BattleSlot, b : BattleSlot) -> bool: + return a.mon.get_stat(PokemonNature.Stat.SPEED) > b.mon.get_stat(PokemonNature.Stat.SPEED)) + + for slot in ordered: + if !slot.is_active(): continue + for effect in slot.mon.get_effects(): + if !slot.is_active(): break + await effect.on_turn_end(self, slot) + if check_end(): return + + for slot in slots: + if slot.mon != null: slot.mon.cleanup_volatile() func _replace_fainted() -> void: for slot in slots: @@ -241,9 +301,9 @@ func _replace_fainted() -> void: var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot) if chosen == null or !options.has(chosen): chosen = options[0] slot.set_pokemon(chosen) - await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) + await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) -#---------- requetes (utilisees par les controleurs et TargetResolver) ---------- +#---------- requetes (utilisees par les controleurs, les effets et TargetResolver) ---------- func random_pick(items : Array) -> Variant: #tirage dans le generateur du combat (et non le hasard global) : un meme seed rejoue exactement le meme combat diff --git a/game/battle/battle_ai.gd b/game/battle/battle_ai.gd index 8d09d3e..2f05850 100644 --- a/game/battle/battle_ai.gd +++ b/game/battle/battle_ai.gd @@ -51,7 +51,7 @@ func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction: if options.is_empty(): return BattleAction.use_struggle(slot) - var chosen : Array = options.pick_random() if battle.rng.randf() < randomness else best + var chosen : Array = battle.random_pick(options) if battle.rng.randf() < randomness else best return BattleAction.use_move(slot, chosen[0], chosen[1]) func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance: @@ -139,7 +139,7 @@ func _types_of(pokemon : PokemonInstance) -> Array[String]: func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Variant) -> float: var targets : Array[BattleSlot] = TargetResolver.resolve(battle, slot, move, candidate) if targets.is_empty(): return -1.0 - if move.power <= 0 or move.category == "status": return 0.1 #pas d'effet implemente : dernier recours + if move.power <= 0 or move.category == "status": return _status_score(slot, move, targets) var accuracy : float = 1.0 if move.accuracy <= 0 else move.accuracy / 100.0 var spread : bool = targets.size() > 1 @@ -154,3 +154,31 @@ func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Var score += value if damage >= remaining: score += KILL_SCORE return score + +func _status_score(slot : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> float: + #valeur approximative d'une capacite de statut : toujours bien inferieure a une bonne attaque, + #mais choisie quand elle apporte vraiment quelque chose (statut sur un adversaire sain, boost utile, soin) + var score : float = 0.1 + for t in targets: + var enemy : bool = t.side != slot.side + + if move.ailment != "none" and enemy: + var status : int = StatusRegistry.from_ailment(move.ailment, move) + if status != -1: + if t.mon.pokemon.status == PokemonInstance.Status.NONE and !StatusRegistry.is_immune(t.mon.get_types(), status): + score += 12.0 + elif move.ailment == "confusion" and !t.mon.has_volatile(&"confusion"): + score += 8.0 + + for sc in move.stat_changes: + var key : Variant = BattleMon.STAT_KEYS.get(sc.stat) + if key == null: continue + var stage : int = t.mon.get_stage(key) + var helpful : bool = (sc.change > 0 and !enemy) or (sc.change < 0 and enemy) + if helpful and absi(stage) < 2: score += 5.0 * absi(sc.change) + elif !helpful: score -= 10.0 + + if move.healing > 0 and !enemy: + var missing : float = 1.0 - float(t.mon.pokemon.current_hp) / float(t.mon.pokemon.get_max_hp()) + score += 30.0 * missing + return score diff --git a/game/battle/battle_event.gd b/game/battle/battle_event.gd index ca3aa79..ddc5580 100644 --- a/game/battle/battle_event.gd +++ b/game/battle/battle_event.gd @@ -10,6 +10,7 @@ enum Type { MOVE_USED, MOVE_MISSED, DAMAGE, CRITICAL, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT, FAINTED, FLED, MESSAGE, + STATUS_CHANGED, HEAL, } var type : Type = Type.MESSAGE @@ -18,7 +19,7 @@ var target_slot : int = -1 var source_name : String = "" var target_name : String = "" var move : MoveData = null -var amount : int = 0 #degats, numero de tour... +var amount : int = 0 #degats, soin, numero de tour, nouveau statut (STATUS_CHANGED : 0 = gueri)... var hp : int = 0 #PV restants de la cible var max_hp : int = 0 var text : String = "" @@ -51,5 +52,5 @@ func describe() -> String: Type.NO_EFFECT: return "Ça n'affecte pas %s..." % target_name Type.FAINTED: return "%s est K.O. !" % source_name Type.FLED: return "Vous prenez la fuite !" - Type.MESSAGE: return text + Type.MESSAGE, Type.STATUS_CHANGED, Type.HEAL: return text return "" diff --git a/game/battle/battle_mon.gd b/game/battle/battle_mon.gd index 2a38594..cc64335 100644 --- a/game/battle/battle_mon.gd +++ b/game/battle/battle_mon.gd @@ -1,15 +1,46 @@ class_name BattleMon extends RefCounted -#un pokemon en combat : enveloppe autour du PokemonInstance qui porte l'etat TEMPORAIRE du combat. -#les PV et le K.O. s'appliquent directement au PokemonInstance (ils persistent apres le combat). +#un pokemon en combat : enveloppe autour du PokemonInstance qui porte l'etat TEMPORAIRE du combat +#(boosts, confusion, peur, compteurs...). Disparait au changement de pokemon ou a la fin du combat. +#les PV, le K.O. et le statut principal s'appliquent directement au PokemonInstance (ils persistent). + +const MIN_STAGE : int = -6 +const MAX_STAGE : int = 6 + +#noms PokeAPI -> cle dans "stages" (les stats classiques utilisent PokemonNature.Stat) +const STAT_KEYS : Dictionary = { + "attack": PokemonNature.Stat.ATTACK, + "defense": PokemonNature.Stat.DEFENSE, + "special-attack": PokemonNature.Stat.SP_ATTACK, + "special-defense": PokemonNature.Stat.SP_DEFENSE, + "speed": PokemonNature.Stat.SPEED, + "accuracy": &"accuracy", + "evasion": &"evasion", +} + +const STAT_LABELS : Dictionary = { + "attack": "L'Attaque", + "defense": "La Défense", + "special-attack": "L'Attaque Spéciale", + "special-defense": "La Défense Spéciale", + "speed": "La Vitesse", + "accuracy": "La Précision", + "evasion": "L'Esquive", +} var pokemon : PokemonInstance var slot_id : int = -1 -var stages : Dictionary = {} #PokemonNature.Stat -> -6..6 (boosts/malus ; appliques par les effets, V2) +var stages : Dictionary = {} #cle (PokemonNature.Stat, &"accuracy" ou &"evasion") -> -6..6 +var volatile : Array[BattleEffect] = [] #effets temporaires : confusion, peur... +var status_turns : int = 0 #sommeil : nombre de tentatives d'action restantes +var toxic_counter : int = 0 #poison grave : nombre de tours ecoules +var acted_this_turn : bool = false func _init(p_pokemon : PokemonInstance) -> void: pokemon = p_pokemon + if pokemon.status == PokemonInstance.Status.SLEEP: + status_turns = 2 #deja endormi en entrant en combat func display_name() -> String: return pokemon.get_display_name() @@ -23,11 +54,61 @@ func get_types() -> Array[String]: result.append(TypeChart.type_name(t)) return result -func get_stage(stat : PokemonNature.Stat) -> int: - return int(stages.get(stat, 0)) +#---------- effets ---------- +func get_effects() -> Array[BattleEffect]: + #statut principal + effets temporaires (nouveau tableau a chaque appel : on peut modifier pendant l'iteration) + var result : Array[BattleEffect] = [] + var status_effect : BattleEffect = StatusRegistry.effect_for(int(pokemon.status)) + if status_effect != null: result.append(status_effect) + result.append_array(volatile) + return result + +func has_volatile(id : StringName) -> bool: + for e in volatile: + if e.id == id: return true + return false + +func add_volatile(effect : BattleEffect) -> void: + volatile.append(effect) + +func remove_volatile(id : StringName) -> void: + for i in range(volatile.size() - 1, -1, -1): + if volatile[i].id == id: volatile.remove_at(i) + +func cleanup_volatile() -> void: + for i in range(volatile.size() - 1, -1, -1): + if volatile[i].is_expired(): volatile.remove_at(i) + +func on_status_applied(status : int, rng : RandomNumberGenerator) -> void: + toxic_counter = 0 + status_turns = rng.randi_range(1, 3) if status == PokemonInstance.Status.SLEEP else 0 + +#---------- statistiques et boosts ---------- + +func get_stage(key : Variant) -> int: + return int(stages.get(key, 0)) + +## Applique un changement de boost ; renvoie le changement reellement applique (0 si deja au maximum/minimum). +func change_stage(stat_name : String, delta : int) -> int: + if !STAT_KEYS.has(stat_name): return 0 + var key : Variant = STAT_KEYS[stat_name] + var current : int = get_stage(key) + var updated : int = clampi(current + delta, MIN_STAGE, MAX_STAGE) + stages[key] = updated + return updated - current + +static func _stage_multiplier(stage : int) -> float: + return (2.0 + stage) / 2.0 if stage >= 0 else 2.0 / (2.0 - stage) + +## Stat effective : stat du pokemon x boost, puis modifications des effets (brulure, paralysie...). func get_stat(stat : PokemonNature.Stat) -> float: - #stat effective = stat du pokemon x multiplicateur de boost - var stage : int = get_stage(stat) - var multiplier : float = (2.0 + stage) / 2.0 if stage >= 0 else 2.0 / (2.0 - stage) - return pokemon.get_stat(stat) * multiplier + var value : float = pokemon.get_stat(stat) * _stage_multiplier(get_stage(stat)) + for effect in get_effects(): + value = effect.modify_stat(self, stat, value) + return value + +## Multiplicateur de precision de cette attaque contre une cible (boost de precision - esquive). +func accuracy_multiplier_against(target : BattleMon) -> float: + var stage : int = clampi(get_stage(&"accuracy") - target.get_stage(&"evasion"), MIN_STAGE, MAX_STAGE) + return (3.0 + stage) / 3.0 if stage >= 0 else 3.0 / (3.0 - stage) diff --git a/game/battle/battle_ui.gd b/game/battle/battle_ui.gd index d01f2fb..9bce22d 100644 --- a/game/battle/battle_ui.gd +++ b/game/battle/battle_ui.gd @@ -28,12 +28,14 @@ var _skip_requested : bool = false var _prev_mouse_mode : Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _mouse_saved : bool = false +var _debug_messages : int = 0 #suivi temporaire func _ready() -> void: layer = 50 process_mode = Node.PROCESS_MODE_ALWAYS #le monde est en pause pendant le combat _build_ui() - + print("[BattleUI] pret (layer %d, paused=%s)" % [layer, get_tree().paused]) #suivi : a retirer ensuite + _prev_mouse_mode = Input.mouse_mode _mouse_saved = true Input.mouse_mode = Input.MOUSE_MODE_VISIBLE @@ -119,6 +121,9 @@ func _corner(parent : Control, preset : Control.LayoutPreset, margins : Dictiona #---------- messages ---------- func show_message(text : String, duration : float = 1.1) -> void: + if _debug_messages < 4: #suivi temporaire : les 4 premiers messages seulement + _debug_messages += 1 + print("[BattleUI] message : " + text) _text_label.text = text _text_panel.show() await _wait(duration) @@ -147,7 +152,13 @@ func set_slot_pokemon(slot : BattleSlot) -> void: var name_label := Label.new() name_label.text = "%s Lv.%d" % [mon.display_name(), mon.pokemon.level] name_label.add_theme_font_size_override("font_size", 20) - box.add_child(name_label) + var name_row := HBoxContainer.new() + name_row.add_theme_constant_override("separation", 10) + box.add_child(name_row) + name_row.add_child(name_label) + var status_label := Label.new() + status_label.add_theme_font_size_override("font_size", 16) + name_row.add_child(status_label) var bar := ProgressBar.new() bar.show_percentage = false @@ -167,9 +178,24 @@ func set_slot_pokemon(slot : BattleSlot) -> void: box.add_child(hp_label) (_player_panels if slot.side == 0 else _enemy_panels).add_child(panel) - var entry : Dictionary = {"panel": panel, "bar": bar, "fill": fill, "hp_label": hp_label} + var entry : Dictionary = {"panel": panel, "bar": bar, "fill": fill, "hp_label": hp_label, "status_label": status_label} _panels[slot.id] = entry _set_hp(entry, mon.pokemon.current_hp, mon.pokemon.get_max_hp(), false) + set_slot_status(slot.id, int(mon.pokemon.status)) + +func set_slot_status(slot_id : int, status : int) -> void: + #badge de statut a cote du nom (brulure, paralysie...) ; vide si le pokemon n'a aucun statut + var entry : Dictionary = _panels.get(slot_id, {}) + if entry.is_empty(): return + var label : Label = entry["status_label"] + var badge : Variant = TeamHUD.STATUS_BADGES.get(status) + if badge == null: + label.text = "" + return + label.text = badge[0] + label.add_theme_color_override("font_color", badge[1]) + label.add_theme_constant_override("outline_size", 4) + label.add_theme_color_override("font_outline_color", Color.BLACK) func animate_hp(slot_id : int, hp : int, max_hp : int) -> void: var entry : Dictionary = _panels.get(slot_id, {}) diff --git a/game/battle/battle_world_presenter.gd b/game/battle/battle_world_presenter.gd index 3ad32d9..5deb41d 100644 --- a/game/battle/battle_world_presenter.gd +++ b/game/battle/battle_world_presenter.gd @@ -77,6 +77,7 @@ func _start_camera(max_slots : int) -> void: _camera.global_transform = _previous_camera.global_transform if _previous_camera else final_transform if _previous_camera: _camera.fov = _previous_camera.fov _camera.make_current() + print("[BattleCamera] camera du jeu : %s | camera de combat : parent=%s, current=%s" % [_previous_camera, _camera.get_parent().get_path(), _camera.current]) #suivi : a retirer ensuite var tween : Tween = ui.create_tween() tween.tween_property(_camera, "global_transform", final_transform, camera_move_time).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) @@ -130,6 +131,14 @@ func present(event : BattleEvent) -> void: ui.remove_slot_panel(event.source_slot) await ui.show_message(event.describe(), 1.0) + BattleEvent.Type.STATUS_CHANGED: + ui.set_slot_status(event.target_slot, event.amount) + if event.text != "": await ui.show_message(event.text, 1.0) + + BattleEvent.Type.HEAL: + await ui.animate_hp(event.target_slot, event.hp, event.max_hp) + if event.text != "": await ui.show_message(event.text, 1.0) + BattleEvent.Type.TURN_START: ui.hide_message() #pas de texte : les menus prennent la suite diff --git a/game/battle/damage_calculator.gd b/game/battle/damage_calculator.gd index f4bd8b5..8d2211f 100644 --- a/game/battle/damage_calculator.gd +++ b/game/battle/damage_calculator.gd @@ -1,7 +1,7 @@ class_name DamageCalculator extends RefCounted -const CRIT_CHANCE : float = 1.0 / 24.0 +const CRIT_CHANCES : Array[float] = [1.0 / 24.0, 1.0 / 8.0, 1.0 / 2.0, 1.0] #selon le bonus de critique de la capacite (0 a 3) const CRIT_MULTIPLIER : float = 1.5 const STAB_MULTIPLIER : float = 1.5 const SPREAD_MULTIPLIER : float = 0.75 #capacite touchant plusieurs cibles @@ -23,7 +23,7 @@ static func calculate(attacker : BattleMon, defender : BattleMon, move : MoveDat #formule officielle var damage : float = floorf(floorf(floorf(2.0 * level / 5.0 + 2.0) * move.power * attack / maxf(defense, 1.0)) / 50.0) + 2.0 - var critical : bool = !deterministic and rng != null and rng.randf() < CRIT_CHANCE + var critical : bool = !deterministic and rng != null and rng.randf() < CRIT_CHANCES[clampi(move.crit_rate, 0, 3)] var roll : float = 0.925 if (deterministic or rng == null) else rng.randi_range(85, 100) / 100.0 var stab : float = STAB_MULTIPLIER if attacker.get_types().has(move.type.to_lower()) else 1.0 @@ -38,3 +38,11 @@ static func calculate(attacker : BattleMon, defender : BattleMon, move : MoveDat static func estimate(attacker : BattleMon, defender : BattleMon, move : MoveData, spread : bool = false) -> int: #degats attendus, sans hasard (utilise par l'IA) return int(calculate(attacker, defender, move, null, spread, true)["damage"]) + +static func confusion_damage(mon : BattleMon, rng : RandomNumberGenerator) -> int: + #le pokemon confus se blesse : capacite physique de puissance 40, sans type ni critique + var attack : float = mon.get_stat(PokemonNature.Stat.ATTACK) + var defense : float = mon.get_stat(PokemonNature.Stat.DEFENSE) + var damage : float = floorf(floorf(floorf(2.0 * mon.pokemon.level / 5.0 + 2.0) * 40.0 * attack / maxf(defense, 1.0)) / 50.0) + 2.0 + damage *= rng.randi_range(85, 100) / 100.0 + return maxi(1, floori(damage)) diff --git a/game/battle/effects/battle_effect.gd b/game/battle/effects/battle_effect.gd new file mode 100644 index 0000000..d7fe01b --- /dev/null +++ b/game/battle/effects/battle_effect.gd @@ -0,0 +1,24 @@ +class_name BattleEffect +extends RefCounted + +#un effet actif sur un pokemon en combat : statut, confusion, peur... (plus tard : talent, objet tenu). +#le moteur appelle ces crochets aux moments cles ; une sous-classe ne redefinit que ceux qui l'interessent. +#les crochets "async" peuvent utiliser "await" (messages, degats...). + +var id : StringName = &"" + +## Modifie une statistique (brulure : Attaque / 2, paralysie : Vitesse / 2...). Synchrone. +func modify_stat(_mon : BattleMon, _stat : PokemonNature.Stat, value : float) -> float: + return value + +## Appele juste avant qu'un pokemon utilise une capacite. Renvoyer false = il ne peut pas agir. +func before_action(_battle : Battle, _slot : BattleSlot, _action : BattleAction) -> bool: + return true + +## Appele a la fin de chaque tour (degats de brulure, de poison...). +func on_turn_end(_battle : Battle, _slot : BattleSlot) -> void: + pass + +## Pour les effets temporaires : true = retire a la fin du tour (peur). +func is_expired() -> bool: + return false diff --git a/game/battle/effects/battle_effect.gd.uid b/game/battle/effects/battle_effect.gd.uid new file mode 100644 index 0000000..e8c9371 --- /dev/null +++ b/game/battle/effects/battle_effect.gd.uid @@ -0,0 +1 @@ +uid://cjhxvijo8je4c diff --git a/game/battle/effects/burn_effect.gd b/game/battle/effects/burn_effect.gd new file mode 100644 index 0000000..3b32100 --- /dev/null +++ b/game/battle/effects/burn_effect.gd @@ -0,0 +1,12 @@ +class_name BurnEffect +extends BattleEffect + +func _init() -> void: + id = &"burn" + +func modify_stat(_mon : BattleMon, stat : PokemonNature.Stat, value : float) -> float: + return value * 0.5 if stat == PokemonNature.Stat.ATTACK else value + +func on_turn_end(battle : Battle, slot : BattleSlot) -> void: + var amount : int = maxi(1, floori(slot.mon.pokemon.get_max_hp() / 16.0)) + await battle.hurt(slot, amount, "%s souffre de sa brûlure !" % slot.mon.display_name()) diff --git a/game/battle/effects/burn_effect.gd.uid b/game/battle/effects/burn_effect.gd.uid new file mode 100644 index 0000000..0817bff --- /dev/null +++ b/game/battle/effects/burn_effect.gd.uid @@ -0,0 +1 @@ +uid://drr1643p1u111 diff --git a/game/battle/effects/confusion_effect.gd b/game/battle/effects/confusion_effect.gd new file mode 100644 index 0000000..f67c2eb --- /dev/null +++ b/game/battle/effects/confusion_effect.gd @@ -0,0 +1,27 @@ +class_name ConfusionEffect +extends BattleEffect + +#effet temporaire (disparait au changement de pokemon) : 1 chance sur 3 de se blesser au lieu d'attaquer + +const SELF_HIT_CHANCE : float = 1.0 / 3.0 + +var turns : int = 3 + +func _init(p_turns : int = 3) -> void: + id = &"confusion" + turns = p_turns + +func before_action(battle : Battle, slot : BattleSlot, _action : BattleAction) -> bool: + var mon : BattleMon = slot.mon + if turns <= 0: + mon.remove_volatile(id) + await battle.message("%s n'est plus confus !" % mon.display_name()) + return true + + turns -= 1 + await battle.message("%s est confus !" % mon.display_name()) + if battle.rng.randf() < SELF_HIT_CHANCE: + var damage : int = DamageCalculator.confusion_damage(mon, battle.rng) + await battle.hurt(slot, damage, "Il se blesse dans sa confusion !") + return false + return true diff --git a/game/battle/effects/confusion_effect.gd.uid b/game/battle/effects/confusion_effect.gd.uid new file mode 100644 index 0000000..eda3b12 --- /dev/null +++ b/game/battle/effects/confusion_effect.gd.uid @@ -0,0 +1 @@ +uid://dfleojc0ag7eq diff --git a/game/battle/effects/flinch_effect.gd b/game/battle/effects/flinch_effect.gd new file mode 100644 index 0000000..8bf041f --- /dev/null +++ b/game/battle/effects/flinch_effect.gd @@ -0,0 +1,14 @@ +class_name FlinchEffect +extends BattleEffect + +#peur : le pokemon ne peut pas agir ce tour-ci (s'il n'a pas encore agi). Disparait a la fin du tour. + +func _init() -> void: + id = &"flinch" + +func before_action(battle : Battle, slot : BattleSlot, _action : BattleAction) -> bool: + await battle.message("%s a peur et ne peut pas attaquer !" % slot.mon.display_name()) + return false + +func is_expired() -> bool: + return true diff --git a/game/battle/effects/flinch_effect.gd.uid b/game/battle/effects/flinch_effect.gd.uid new file mode 100644 index 0000000..b0a2421 --- /dev/null +++ b/game/battle/effects/flinch_effect.gd.uid @@ -0,0 +1 @@ +uid://d3wtsfx3xjd15 diff --git a/game/battle/effects/freeze_effect.gd b/game/battle/effects/freeze_effect.gd new file mode 100644 index 0000000..458dd09 --- /dev/null +++ b/game/battle/effects/freeze_effect.gd @@ -0,0 +1,17 @@ +class_name FreezeEffect +extends BattleEffect + +const THAW_CHANCE : float = 0.2 + +func _init() -> void: + id = &"freeze" + +func before_action(battle : Battle, slot : BattleSlot, action : BattleAction) -> bool: + var mon : BattleMon = slot.mon + #une capacite de type Feu degele le pokemon, et chaque tour il a 20 % de chances de degeler + var fire_move : bool = action.move != null and action.move.type.to_lower() == "fire" + if fire_move or battle.rng.randf() < THAW_CHANCE: + await battle.cure_status(slot, "%s dégèle !" % mon.display_name()) + return true + await battle.message("%s est gelé ! Il ne peut pas bouger." % mon.display_name()) + return false diff --git a/game/battle/effects/freeze_effect.gd.uid b/game/battle/effects/freeze_effect.gd.uid new file mode 100644 index 0000000..b972de3 --- /dev/null +++ b/game/battle/effects/freeze_effect.gd.uid @@ -0,0 +1 @@ +uid://upmubyjayuqe diff --git a/game/battle/effects/paralysis_effect.gd b/game/battle/effects/paralysis_effect.gd new file mode 100644 index 0000000..651becb --- /dev/null +++ b/game/battle/effects/paralysis_effect.gd @@ -0,0 +1,16 @@ +class_name ParalysisEffect +extends BattleEffect + +const FULL_PARALYSIS_CHANCE : float = 0.25 + +func _init() -> void: + id = &"paralysis" + +func modify_stat(_mon : BattleMon, stat : PokemonNature.Stat, value : float) -> float: + return value * 0.5 if stat == PokemonNature.Stat.SPEED else value + +func before_action(battle : Battle, slot : BattleSlot, _action : BattleAction) -> bool: + if battle.rng.randf() < FULL_PARALYSIS_CHANCE: + await battle.message("%s est paralysé ! Il ne peut pas attaquer !" % slot.mon.display_name()) + return false + return true diff --git a/game/battle/effects/paralysis_effect.gd.uid b/game/battle/effects/paralysis_effect.gd.uid new file mode 100644 index 0000000..abc7316 --- /dev/null +++ b/game/battle/effects/paralysis_effect.gd.uid @@ -0,0 +1 @@ +uid://bv4iyk174vat5 diff --git a/game/battle/effects/poison_effect.gd b/game/battle/effects/poison_effect.gd new file mode 100644 index 0000000..163d30c --- /dev/null +++ b/game/battle/effects/poison_effect.gd @@ -0,0 +1,21 @@ +class_name PoisonEffect +extends BattleEffect + +#poison simple (1/8 des PV par tour) ou poison grave (1/16 x compteur, qui augmente chaque tour) + +var severe : bool = false + +func _init(p_severe : bool = false) -> void: + severe = p_severe + id = &"bad_poison" if severe else &"poison" + +func on_turn_end(battle : Battle, slot : BattleSlot) -> void: + var mon : BattleMon = slot.mon + var max_hp : int = mon.pokemon.get_max_hp() + var amount : int + if severe: + mon.toxic_counter = mini(mon.toxic_counter + 1, 15) + amount = maxi(1, floori(max_hp * mon.toxic_counter / 16.0)) + else: + amount = maxi(1, floori(max_hp / 8.0)) + await battle.hurt(slot, amount, "%s souffre du poison !" % mon.display_name()) diff --git a/game/battle/effects/poison_effect.gd.uid b/game/battle/effects/poison_effect.gd.uid new file mode 100644 index 0000000..ff4d174 --- /dev/null +++ b/game/battle/effects/poison_effect.gd.uid @@ -0,0 +1 @@ +uid://7dqdniguqb3n diff --git a/game/battle/effects/sleep_effect.gd b/game/battle/effects/sleep_effect.gd new file mode 100644 index 0000000..75527f2 --- /dev/null +++ b/game/battle/effects/sleep_effect.gd @@ -0,0 +1,16 @@ +class_name SleepEffect +extends BattleEffect + +#le pokemon dort pendant status_turns tentatives d'action (1 a 3), puis se reveille et agit + +func _init() -> void: + id = &"sleep" + +func before_action(battle : Battle, slot : BattleSlot, _action : BattleAction) -> bool: + var mon : BattleMon = slot.mon + if mon.status_turns > 0: + mon.status_turns -= 1 + await battle.message("%s dort profondément." % mon.display_name()) + return false + await battle.cure_status(slot, "%s se réveille !" % mon.display_name()) + return true diff --git a/game/battle/effects/sleep_effect.gd.uid b/game/battle/effects/sleep_effect.gd.uid new file mode 100644 index 0000000..3248000 --- /dev/null +++ b/game/battle/effects/sleep_effect.gd.uid @@ -0,0 +1 @@ +uid://bf3jsbukymc6v diff --git a/game/battle/effects/status_registry.gd b/game/battle/effects/status_registry.gd new file mode 100644 index 0000000..3a07d13 --- /dev/null +++ b/game/battle/effects/status_registry.gd @@ -0,0 +1,49 @@ +class_name StatusRegistry +extends RefCounted + +#correspondances entre les statuts principaux (PokemonInstance.Status) et leurs effets. +#les effets de statut n'ont pas d'etat propre (les compteurs sont sur BattleMon) : ils sont partages. + +static var _effects : Dictionary = {} + +static func effect_for(status : int) -> BattleEffect: + if status == PokemonInstance.Status.NONE: return null + if _effects.is_empty(): + _effects[PokemonInstance.Status.BURN] = BurnEffect.new() + _effects[PokemonInstance.Status.PARALYSIS] = ParalysisEffect.new() + _effects[PokemonInstance.Status.POISON] = PoisonEffect.new(false) + _effects[PokemonInstance.Status.BAD_POISON] = PoisonEffect.new(true) + _effects[PokemonInstance.Status.SLEEP] = SleepEffect.new() + _effects[PokemonInstance.Status.FREEZE] = FreezeEffect.new() + return _effects.get(status) + +## Nom PokeAPI du statut ("burn", "sleep"...) -> PokemonInstance.Status, ou -1 si ce n'est pas un statut principal. +static func from_ailment(ailment : String, move : MoveData = null) -> int: + match ailment: + "burn": return PokemonInstance.Status.BURN + "paralysis": return PokemonInstance.Status.PARALYSIS + "freeze": return PokemonInstance.Status.FREEZE + "sleep": return PokemonInstance.Status.SLEEP + "poison": + #Toxik et Crochet Venin inflige le poison grave + var severe : bool = move != null and (move.api_name == "toxic" or move.api_name == "poison-fang") + return PokemonInstance.Status.BAD_POISON if severe else PokemonInstance.Status.POISON + return -1 + +static func is_immune(types : Array[String], status : int) -> bool: + match status: + PokemonInstance.Status.BURN: return types.has("fire") + PokemonInstance.Status.FREEZE: return types.has("ice") + PokemonInstance.Status.PARALYSIS: return types.has("electric") + PokemonInstance.Status.POISON, PokemonInstance.Status.BAD_POISON: return types.has("poison") or types.has("steel") + return false + +static func apply_message(status : int, pokemon_name : String) -> String: + match status: + PokemonInstance.Status.BURN: return "%s est brûlé !" % pokemon_name + PokemonInstance.Status.PARALYSIS: return "%s est paralysé ! Il aura du mal à attaquer !" % pokemon_name + PokemonInstance.Status.FREEZE: return "%s est gelé !" % pokemon_name + PokemonInstance.Status.SLEEP: return "%s s'endort !" % pokemon_name + PokemonInstance.Status.POISON: return "%s est empoisonné !" % pokemon_name + PokemonInstance.Status.BAD_POISON: return "%s est gravement empoisonné !" % pokemon_name + return "" diff --git a/game/battle/effects/status_registry.gd.uid b/game/battle/effects/status_registry.gd.uid new file mode 100644 index 0000000..3cdad24 --- /dev/null +++ b/game/battle/effects/status_registry.gd.uid @@ -0,0 +1 @@ +uid://e6oixl35p3ta diff --git a/game/battle/move_executor.gd b/game/battle/move_executor.gd new file mode 100644 index 0000000..35e3d12 --- /dev/null +++ b/game/battle/move_executor.gd @@ -0,0 +1,175 @@ +class_name MoveExecutor +extends RefCounted + +#execute une capacite : precision, degats, coups multiples, puis effets guides par les donnees de MoveData +#(statut, changements de stats, vol de PV, contrecoup, soin, peur...). +#les cas tres particuliers (Abri, Vol, Clonage...) viendront en scripts a part. + +static func execute(battle : Battle, action : BattleAction) -> void: + var actor : BattleSlot = action.slot + var move : MoveData = action.move + + #crochets "avant l'action" : sommeil, gel, paralysie, peur, confusion... (pas de PP consommes si le pokemon n'agit pas) + if !await battle.run_before_action(action): return + + if action.move_slot != null and !action.move_slot.use(): + await battle.message("%s n'a plus de PP pour cette capacité !" % actor.mon.display_name()) + return + + var used := BattleEvent.make(BattleEvent.Type.MOVE_USED, actor) + used.move = move + await battle.emit_event(used) + + var targets : Array[BattleSlot] = TargetResolver.resolve(battle, actor, move, action.chosen_target) + if targets.is_empty(): + await battle.message("Mais cela échoue !") + return + + var damaging : bool = (move.power > 0 or move.meta_category == "ohko") and move.category != "status" + if damaging: + await _use_damaging(battle, actor, move, targets) + else: + await _use_status(battle, actor, move, targets) + + #Lutte : contrecoup pour l'utilisateur + if move.api_name == "struggle" and actor.is_active(): + await battle.hurt(actor, maxi(1, floori(actor.mon.pokemon.get_max_hp() / 4.0)), "%s est blessé par le contrecoup !" % actor.mon.display_name()) + +#---------- capacites offensives ---------- + +static func _use_damaging(battle : Battle, actor : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> void: + var spread : bool = targets.size() > 1 + var total_dealt : int = 0 + + for target in targets: + if !target.is_active(): continue + + if move.meta_category == "ohko": + total_dealt += await _use_ohko(battle, actor, target, move) + if battle.check_end(): break + continue + + if !_hits(battle, actor, target, move): + await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target)) + continue + + var hits : int = _roll_hit_count(battle, move) + var landed : int = 0 + var effectiveness : float = 1.0 + for i in hits: + if !target.is_active(): break + var calc : Dictionary = DamageCalculator.calculate(actor.mon, target.mon, move, battle.rng, spread) + effectiveness = calc["effectiveness"] + if effectiveness == 0.0: break + total_dealt += await battle.deal_damage(actor, target, int(calc["damage"])) + landed += 1 + if calc["critical"]: await battle.emit_event(BattleEvent.make(BattleEvent.Type.CRITICAL, actor, target)) + + if landed == 0: + if effectiveness == 0.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target)) + continue + + if landed > 1: await battle.message("Touché %d fois !" % landed) + if effectiveness > 1.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.SUPER_EFFECTIVE, actor, target)) + elif effectiveness < 1.0: await battle.emit_event(BattleEvent.make(BattleEvent.Type.NOT_VERY_EFFECTIVE, actor, target)) + + if target.mon.is_fainted(): + await battle.emit_event(BattleEvent.make(BattleEvent.Type.FAINTED, target)) + else: + await _secondary_effects(battle, actor, target, move) + + if battle.check_end(): break + + #vol de PV (drain > 0) ou contrecoup (drain < 0), proportionnels aux degats infliges + if total_dealt > 0 and move.drain != 0 and actor.is_active(): + var amount : int = maxi(1, floori(total_dealt * absf(move.drain) / 100.0)) + if move.drain > 0: + await battle.heal(actor, amount, "%s a absorbé de l'énergie !" % actor.mon.display_name()) + else: + await battle.hurt(actor, amount, "%s est blessé par le contrecoup !" % actor.mon.display_name()) + +static func _use_ohko(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> int: + #capacites K.O. en un coup : echouent si la cible est d'un niveau superieur ; precision 30 % + ecart de niveau + var level_gap : int = actor.mon.pokemon.level - target.mon.pokemon.level + if level_gap < 0: + await battle.message("Mais cela échoue !") + return 0 + if TypeChart.get_multiplier(move.type, target.mon.get_types()) == 0.0: + await battle.emit_event(BattleEvent.make(BattleEvent.Type.NO_EFFECT, actor, target)) + return 0 + if battle.rng.randi_range(1, 100) > 30 + level_gap: + await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target)) + return 0 + + var dealt : int = await battle.deal_damage(actor, target, target.mon.pokemon.current_hp) + await battle.message("K.O. en un coup !") + if target.mon.is_fainted(): await battle.emit_event(BattleEvent.make(BattleEvent.Type.FAINTED, target)) + return dealt + +## Effets secondaires d'une capacite offensive, appliques a une cible encore en vie. +static func _secondary_effects(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> void: + #statut + if move.ailment != "none" and move.ailment_chance > 0 and target.is_active(): + if battle.rng.randi_range(1, 100) <= move.ailment_chance: + await battle.inflict_status(actor, target, move.ailment, move, false) + + #peur : seulement si la cible n'a pas encore agi ce tour + if move.flinch_chance > 0 and target.is_active() and !target.mon.acted_this_turn: + if battle.rng.randi_range(1, 100) <= move.flinch_chance and !target.mon.has_volatile(&"flinch"): + target.mon.add_volatile(FlinchEffect.new()) + + #changements de stats : "damage+raise" touche l'utilisateur, "damage+lower" la cible + if !move.stat_changes.is_empty() and battle.rng.randi_range(1, 100) <= move.get_stat_chance(): + var affected : BattleSlot = actor if move.meta_category == "damage+raise" else target + for sc in move.stat_changes: + await battle.change_stat(affected, sc.stat, sc.change) + +#---------- capacites de statut ---------- + +static func _use_status(battle : Battle, actor : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> void: + var handled : bool = false + + for target in targets: + if !target.is_active(): continue + if target != actor and !_hits(battle, actor, target, move): + await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target)) + continue + + if move.ailment != "none": + handled = true + var chance : int = move.get_ailment_chance() + if battle.rng.randi_range(1, 100) <= chance: + await battle.inflict_status(actor, target, move.ailment, move, true) + + if !move.stat_changes.is_empty(): + handled = true + if battle.rng.randi_range(1, 100) <= move.get_stat_chance(): + for sc in move.stat_changes: + await battle.change_stat(target, sc.stat, sc.change) + + if move.healing > 0: + handled = true + var amount : int = maxi(1, floori(target.mon.pokemon.get_max_hp() * move.healing / 100.0)) + var healed : int = await battle.heal(target, amount, "%s récupère des PV !" % target.mon.display_name()) + if healed == 0: + await battle.message("Les PV de %s sont déjà au maximum !" % target.mon.display_name()) + + if !handled: + await battle.message("Mais cela n'a pas d'effet pour l'instant.") + +#---------- precision et coups multiples ---------- + +static func _hits(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> bool: + #precision 0 = ne rate jamais (valeur PokeAPI "null") ; sinon precision de la capacite x boosts de precision/esquive + if move.accuracy <= 0: return true + var chance : float = move.accuracy * actor.mon.accuracy_multiplier_against(target.mon) + return battle.rng.randi_range(1, 100) <= int(chance) + +static func _roll_hit_count(battle : Battle, move : MoveData) -> int: + if move.max_hits <= 1: return 1 + if move.min_hits >= move.max_hits: return move.max_hits + if move.min_hits == 2 and move.max_hits == 5: + #repartition officielle : 2 ou 3 coups 35 % chacun, 4 ou 5 coups 15 % chacun + var roll : int = battle.rng.randi_range(1, 100) + return 2 if roll <= 35 else (3 if roll <= 70 else (4 if roll <= 85 else 5)) + return battle.rng.randi_range(move.min_hits, move.max_hits) diff --git a/game/battle/move_executor.gd.uid b/game/battle/move_executor.gd.uid new file mode 100644 index 0000000..ed26d12 --- /dev/null +++ b/game/battle/move_executor.gd.uid @@ -0,0 +1 @@ +uid://cvr1yldqosrt2 diff --git a/game/world/battle.tscn b/game/world/battle.tscn index 264c155..5e145db 100644 --- a/game/world/battle.tscn +++ b/game/world/battle.tscn @@ -4,3 +4,4 @@ [node name="Battle" type="Node" unique_id=520840168] script = ExtResource("1_5bjgp") +mass_simulation_count = 100