class_name Battle extends RefCounted #moteur de combat au tour par tour, sans aucun visuel. #gere n emplacements par camp (1v1, 2v2, 1 contre plusieurs, plusieurs contre 1). # #usage : # var battle := Battle.new(setup, mon_presenter) # 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) enum Outcome { NONE, PLAYER_WON, PLAYER_LOST, FLED, DRAW } const PLAYER_SIDE : int = 0 static var _struggle : MoveData = null 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] = [] var field : BattleField = BattleField.new() #meteo (terrains et barrieres plus tard) func _init(p_setup : BattleSetup, p_presenter : BattlePresenter = null) -> void: setup = p_setup presenter = p_presenter if setup.seed_value != 0: rng.seed = setup.seed_value else: rng.randomize() _build_slots() func _build_slots() -> void: var per_side_count : Dictionary = {} for participant in setup.participants: for pokemon in participant.pick_starters(): var slot := BattleSlot.new() slot.id = slots.size() slot.side = participant.side slot.index = int(per_side_count.get(participant.side, 0)) per_side_count[participant.side] = slot.index + 1 slot.participant = participant _place(slot, pokemon) slots.append(slot) #---------- boucle principale ---------- func run() -> Outcome: await emit_event(BattleEvent.make(BattleEvent.Type.BATTLE_START)) for slot in slots: await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) for slot in _slots_by_speed(): await _run_on_enter(slot) #talents d'entree (Intimidation, Crachin...) if check_end(): 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 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 outcome != Outcome.NONE: break await _end_of_turn() if outcome != Outcome.NONE: break await _replace_fainted() if check_end(): break return await _finish() func _finish() -> Outcome: var end_event := BattleEvent.make(BattleEvent.Type.BATTLE_END) match 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_event(end_event) return outcome #---------- evenements et aides pour les effets ---------- func emit_event(event : BattleEvent) -> void: event_emitted.emit(event) if presenter != null: await presenter.present(event) ## Affiche un message. func message(text : String) -> void: var e := BattleEvent.make(BattleEvent.Type.MESSAGE) e.text = text 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 ## Annonce qu'un talent se declenche. func announce_ability(slot : BattleSlot) -> void: if slot.mon == null or slot.mon.pokemon.ability == null: return await message("%s utilise son talent %s !" % [slot.mon.display_name(), slot.mon.pokemon.ability.get_display_name()]) ## Change la meteo. Renvoie false si c'est deja la meteo en cours (la capacite ou le talent echoue). func set_weather(weather : BattleField.Weather, _source : BattleSlot = null, turns : int = BattleField.DEFAULT_WEATHER_TURNS) -> bool: if weather == BattleField.Weather.NONE or field.weather == weather: return false field.set_weather(weather, turns) var event := BattleEvent.make(BattleEvent.Type.WEATHER_CHANGED) event.amount = weather event.text = BattleField.start_message(weather) await emit_event(event) return true ## 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 ---------- func _side_alive(side : int) -> bool: for participant in setup.participants: if participant.side == side and participant.has_able_pokemon(): return true return false 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) if player_alive and enemy_alive: return false outcome = Outcome.PLAYER_WON if player_alive else Outcome.PLAYER_LOST return true #---------- actions du tour ---------- func _collect_actions() -> Array[BattleAction]: var actions : Array[BattleAction] = [] for slot in slots: if !slot.is_active(): continue var action : BattleAction = await slot.participant.controller.choose_action(self, slot) if action == null: action = BattleAction.use_struggle(slot) #securite : un controleur qui ne decide rien actions.append(action) return actions func _sort_actions(actions : Array[BattleAction]) -> void: for a in actions: match a.type: BattleAction.Type.FLEE: a.priority = 8 BattleAction.Type.SWITCH: a.priority = 7 BattleAction.Type.ITEM: a.priority = 6 _: a.priority = a.move.priority if a.move else 0 a.speed = a.slot.mon.get_stat(PokemonNature.Stat.SPEED) a.tiebreak = rng.randf() actions.sort_custom(func(a : BattleAction, b : BattleAction) -> bool: if a.priority != b.priority: return a.priority > b.priority if a.speed != b.speed: return a.speed > b.speed return a.tiebreak > b.tiebreak) 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 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_event(BattleEvent.make(BattleEvent.Type.FLED, action.slot)) else: 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_event(BattleEvent.make(BattleEvent.Type.WITHDRAWN, slot)) _place(slot, action.switch_to) slot.mon.acted_this_turn = true await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) await _run_on_enter(slot) #---------- fin de tour ---------- func _end_of_turn() -> void: #effets de fin de tour (brulure, poison...), du plus rapide au plus lent for slot in _slots_by_speed(): 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 await _weather_end_of_turn() for slot in slots: if slot.mon != null: slot.mon.cleanup_volatile() func _weather_end_of_turn() -> void: if field.weather == BattleField.Weather.NONE: return if field.weather == BattleField.Weather.SANDSTORM: for slot in get_active_slots(): if BattleField.is_sandstorm_immune(slot.mon.get_types()): continue await hurt(slot, maxi(1, floori(slot.mon.pokemon.get_max_hp() / 16.0)), "%s est frappé par la tempête de sable !" % slot.mon.display_name()) if check_end(): return field.weather_turns -= 1 if field.weather_turns <= 0: var ended : BattleField.Weather = field.weather field.set_weather(BattleField.Weather.NONE) var event := BattleEvent.make(BattleEvent.Type.WEATHER_CHANGED) event.amount = BattleField.Weather.NONE event.text = BattleField.end_message(ended) await emit_event(event) func _replace_fainted() -> void: for slot in slots: if slot.mon == null or !slot.mon.is_fainted(): continue var options : Array[PokemonInstance] = get_replacement_options(slot) if options.is_empty(): slot.clear() #plus de remplacant : l'emplacement reste vide continue var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot) if chosen == null or !options.has(chosen): chosen = options[0] _place(slot, chosen) await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot)) await _run_on_enter(slot) #---------- placement et entree en combat ---------- func _place(slot : BattleSlot, pokemon : PokemonInstance) -> void: slot.set_pokemon(pokemon) slot.mon.field = field func _run_on_enter(slot : BattleSlot) -> void: if slot.mon == null: return for effect in slot.mon.get_effects(): if !slot.is_active(): return await effect.on_enter(self, slot) func _slots_by_speed() -> Array[BattleSlot]: 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)) return ordered #---------- 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 return items[rng.randi_range(0, items.size() - 1)] func get_slot(id : int) -> BattleSlot: return slots[id] if id >= 0 and id < slots.size() else null func get_active_slots() -> Array[BattleSlot]: var result : Array[BattleSlot] = [] for s in slots: if s.is_active(): result.append(s) return result func get_opponents(slot : BattleSlot) -> Array[BattleSlot]: var result : Array[BattleSlot] = [] for s in slots: if s.is_active() and s.side != slot.side: result.append(s) return result func get_allies(slot : BattleSlot) -> Array[BattleSlot]: var result : Array[BattleSlot] = [] for s in slots: if s.is_active() and s.side == slot.side and s != slot: result.append(s) return result func get_usable_moves(mon : BattleMon) -> Array[MoveSlot]: var result : Array[MoveSlot] = [] for ms in mon.pokemon.active_moves: if ms.has_pp(): result.append(ms) return result func get_replacement_options(slot : BattleSlot) -> Array[PokemonInstance]: #pokemon aptes de l'equipe du participant qui ne sont pas deja en jeu var result : Array[PokemonInstance] = [] for p in slot.participant.team.members: if p.is_fainted() or _is_in_play(p): continue result.append(p) return result func _is_in_play(pokemon : PokemonInstance) -> bool: for s in slots: if s.mon != null and s.mon.pokemon == pokemon: return true return false #---------- Lutte ---------- static func make_struggle() -> MoveData: if _struggle == null: _struggle = MoveData.new() _struggle.api_name = "struggle" _struggle.names = {"fr": "Lutte", "en": "Struggle"} _struggle.type = "typeless" _struggle.category = "physical" _struggle.power = 50 _struggle.accuracy = 0 _struggle.pp = 1 _struggle.priority = 0 _struggle.target = "random-opponent" return _struggle