Add effects to battle system

This commit is contained in:
TheThomaas 2026-10-18 14:53:28 +02:00
parent 3c91f404c6
commit e666c8a34d
28 changed files with 699 additions and 104 deletions

View file

@ -9,6 +9,9 @@ extends RefCounted
# var outcome : Battle.Outcome = await battle.run() # var outcome : Battle.Outcome = await battle.run()
# #
#a chaque evenement, la presentation est appelee et le combat attend qu'elle ait fini. #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) signal event_emitted(event : BattleEvent)
@ -49,11 +52,11 @@ func _build_slots() -> void:
#---------- boucle principale ---------- #---------- boucle principale ----------
func run() -> Outcome: func run() -> Outcome:
await _emit(BattleEvent.make(BattleEvent.Type.BATTLE_START)) await emit_event(BattleEvent.make(BattleEvent.Type.BATTLE_START))
for slot in slots: 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() return await _finish()
while outcome == Outcome.NONE: while outcome == Outcome.NONE:
@ -63,17 +66,22 @@ func run() -> Outcome:
turn += 1 turn += 1
var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START) var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START)
turn_event.amount = turn 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() var actions : Array[BattleAction] = await _collect_actions()
_sort_actions(actions) _sort_actions(actions)
for action in actions: for action in actions:
await _execute(action) 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 if outcome != Outcome.NONE: break
await _replace_fainted() await _replace_fainted()
if _check_end(): break if check_end(): break
return await _finish() return await _finish()
@ -84,18 +92,120 @@ func _finish() -> Outcome:
Outcome.PLAYER_LOST: end_event.text = "Défaite..." Outcome.PLAYER_LOST: end_event.text = "Défaite..."
Outcome.FLED: end_event.text = "Vous avez pris la fuite." Outcome.FLED: end_event.text = "Vous avez pris la fuite."
Outcome.DRAW: end_event.text = "Match nul (limite de tours atteinte)." Outcome.DRAW: end_event.text = "Match nul (limite de tours atteinte)."
await _emit(end_event) await emit_event(end_event)
return outcome return outcome
func _emit(event : BattleEvent) -> void: #---------- evenements et aides pour les effets ----------
func emit_event(event : BattleEvent) -> void:
event_emitted.emit(event) event_emitted.emit(event)
if presenter != null: if presenter != null:
await presenter.present(event) await presenter.present(event)
func _msg(text : String) -> void: ## Affiche un message.
func message(text : String) -> void:
var e := BattleEvent.make(BattleEvent.Type.MESSAGE) var e := BattleEvent.make(BattleEvent.Type.MESSAGE)
e.text = text 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 ---------- #---------- fin de combat ----------
@ -104,7 +214,7 @@ func _side_alive(side : int) -> bool:
if participant.side == side and participant.has_able_pokemon(): return true if participant.side == side and participant.has_able_pokemon(): return true
return false return false
func _check_end() -> bool: func check_end() -> bool:
if outcome != Outcome.NONE: return true if outcome != Outcome.NONE: return true
var player_alive : bool = _side_alive(PLAYER_SIDE) var player_alive : bool = _side_alive(PLAYER_SIDE)
var enemy_alive : bool = _side_alive(1 - 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: func _execute(action : BattleAction) -> void:
if !action.slot.is_active(): return #mis K.O. avant son tour if !action.slot.is_active(): return #mis K.O. avant son tour
action.slot.mon.acted_this_turn = true
match action.type: 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.SWITCH: await _execute_switch(action)
BattleAction.Type.FLEE: await _execute_flee(action) BattleAction.Type.FLEE: await _execute_flee(action)
func _execute_flee(action : BattleAction) -> void: func _execute_flee(action : BattleAction) -> void:
if setup.can_flee and action.slot.side == PLAYER_SIDE: if setup.can_flee and action.slot.side == PLAYER_SIDE:
outcome = Outcome.FLED outcome = Outcome.FLED
await _emit(BattleEvent.make(BattleEvent.Type.FLED, action.slot)) await emit_event(BattleEvent.make(BattleEvent.Type.FLED, action.slot))
else: else:
await _msg("Impossible de fuir !") await message("Impossible de fuir !")
func _execute_switch(action : BattleAction) -> void: func _execute_switch(action : BattleAction) -> void:
var slot : BattleSlot = action.slot var slot : BattleSlot = action.slot
if !get_replacement_options(slot).has(action.switch_to): return 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) 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: #---------- fin de tour ----------
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))
func _apply_damage(source : BattleSlot, target : BattleSlot, amount : int) -> void: func _end_of_turn() -> void:
var before : int = target.mon.pokemon.current_hp #effets de fin de tour (brulure, poison...), du plus rapide au plus lent
target.mon.pokemon.take_damage(amount) var ordered : Array[BattleSlot] = get_active_slots()
var event := BattleEvent.make(BattleEvent.Type.DAMAGE, source, target) ordered.sort_custom(func(a : BattleSlot, b : BattleSlot) -> bool:
event.amount = before - target.mon.pokemon.current_hp return a.mon.get_stat(PokemonNature.Stat.SPEED) > b.mon.get_stat(PokemonNature.Stat.SPEED))
event.hp = target.mon.pokemon.current_hp
event.max_hp = target.mon.pokemon.get_max_hp() for slot in ordered:
await _emit(event) if !slot.is_active(): continue
for effect in slot.mon.get_effects():
func _hits(move : MoveData) -> bool: if !slot.is_active(): break
#precision 0 = ne rate jamais (valeur PokeAPI "null") await effect.on_turn_end(self, slot)
return move.accuracy <= 0 or rng.randi_range(1, 100) <= move.accuracy if check_end(): return
#---------- remplacements ---------- for slot in slots:
if slot.mon != null: slot.mon.cleanup_volatile()
func _replace_fainted() -> void: func _replace_fainted() -> void:
for slot in slots: for slot in slots:
@ -241,9 +301,9 @@ func _replace_fainted() -> void:
var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot) var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot)
if chosen == null or !options.has(chosen): chosen = options[0] if chosen == null or !options.has(chosen): chosen = options[0]
slot.set_pokemon(chosen) 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: 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 #tirage dans le generateur du combat (et non le hasard global) : un meme seed rejoue exactement le meme combat

View file

@ -51,7 +51,7 @@ func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction:
if options.is_empty(): return BattleAction.use_struggle(slot) 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]) return BattleAction.use_move(slot, chosen[0], chosen[1])
func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance: 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: func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Variant) -> float:
var targets : Array[BattleSlot] = TargetResolver.resolve(battle, slot, move, candidate) var targets : Array[BattleSlot] = TargetResolver.resolve(battle, slot, move, candidate)
if targets.is_empty(): return -1.0 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 accuracy : float = 1.0 if move.accuracy <= 0 else move.accuracy / 100.0
var spread : bool = targets.size() > 1 var spread : bool = targets.size() > 1
@ -154,3 +154,31 @@ func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Var
score += value score += value
if damage >= remaining: score += KILL_SCORE if damage >= remaining: score += KILL_SCORE
return 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

View file

@ -10,6 +10,7 @@ enum Type {
MOVE_USED, MOVE_MISSED, DAMAGE, CRITICAL, MOVE_USED, MOVE_MISSED, DAMAGE, CRITICAL,
SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT,
FAINTED, FLED, MESSAGE, FAINTED, FLED, MESSAGE,
STATUS_CHANGED, HEAL,
} }
var type : Type = Type.MESSAGE var type : Type = Type.MESSAGE
@ -18,7 +19,7 @@ var target_slot : int = -1
var source_name : String = "" var source_name : String = ""
var target_name : String = "" var target_name : String = ""
var move : MoveData = null 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 hp : int = 0 #PV restants de la cible
var max_hp : int = 0 var max_hp : int = 0
var text : String = "" var text : String = ""
@ -51,5 +52,5 @@ func describe() -> String:
Type.NO_EFFECT: return "Ça n'affecte pas %s..." % target_name Type.NO_EFFECT: return "Ça n'affecte pas %s..." % target_name
Type.FAINTED: return "%s est K.O. !" % source_name Type.FAINTED: return "%s est K.O. !" % source_name
Type.FLED: return "Vous prenez la fuite !" Type.FLED: return "Vous prenez la fuite !"
Type.MESSAGE: return text Type.MESSAGE, Type.STATUS_CHANGED, Type.HEAL: return text
return "" return ""

View file

@ -1,15 +1,46 @@
class_name BattleMon class_name BattleMon
extends RefCounted extends RefCounted
#un pokemon en combat : enveloppe autour du PokemonInstance qui porte l'etat TEMPORAIRE du combat. #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). #(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 pokemon : PokemonInstance
var slot_id : int = -1 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: func _init(p_pokemon : PokemonInstance) -> void:
pokemon = p_pokemon pokemon = p_pokemon
if pokemon.status == PokemonInstance.Status.SLEEP:
status_turns = 2 #deja endormi en entrant en combat
func display_name() -> String: func display_name() -> String:
return pokemon.get_display_name() return pokemon.get_display_name()
@ -23,11 +54,61 @@ func get_types() -> Array[String]:
result.append(TypeChart.type_name(t)) result.append(TypeChart.type_name(t))
return result return result
func get_stage(stat : PokemonNature.Stat) -> int: #---------- effets ----------
return int(stages.get(stat, 0))
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: func get_stat(stat : PokemonNature.Stat) -> float:
#stat effective = stat du pokemon x multiplicateur de boost var value : float = pokemon.get_stat(stat) * _stage_multiplier(get_stage(stat))
var stage : int = get_stage(stat) for effect in get_effects():
var multiplier : float = (2.0 + stage) / 2.0 if stage >= 0 else 2.0 / (2.0 - stage) value = effect.modify_stat(self, stat, value)
return pokemon.get_stat(stat) * multiplier 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)

View file

@ -28,12 +28,14 @@ var _skip_requested : bool = false
var _prev_mouse_mode : Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _prev_mouse_mode : Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _mouse_saved : bool = false var _mouse_saved : bool = false
var _debug_messages : int = 0 #suivi temporaire
func _ready() -> void: func _ready() -> void:
layer = 50 layer = 50
process_mode = Node.PROCESS_MODE_ALWAYS #le monde est en pause pendant le combat process_mode = Node.PROCESS_MODE_ALWAYS #le monde est en pause pendant le combat
_build_ui() _build_ui()
print("[BattleUI] pret (layer %d, paused=%s)" % [layer, get_tree().paused]) #suivi : a retirer ensuite
_prev_mouse_mode = Input.mouse_mode _prev_mouse_mode = Input.mouse_mode
_mouse_saved = true _mouse_saved = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
@ -119,6 +121,9 @@ func _corner(parent : Control, preset : Control.LayoutPreset, margins : Dictiona
#---------- messages ---------- #---------- messages ----------
func show_message(text : String, duration : float = 1.1) -> void: 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_label.text = text
_text_panel.show() _text_panel.show()
await _wait(duration) await _wait(duration)
@ -147,7 +152,13 @@ func set_slot_pokemon(slot : BattleSlot) -> void:
var name_label := Label.new() var name_label := Label.new()
name_label.text = "%s Lv.%d" % [mon.display_name(), mon.pokemon.level] name_label.text = "%s Lv.%d" % [mon.display_name(), mon.pokemon.level]
name_label.add_theme_font_size_override("font_size", 20) 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() var bar := ProgressBar.new()
bar.show_percentage = false bar.show_percentage = false
@ -167,9 +178,24 @@ func set_slot_pokemon(slot : BattleSlot) -> void:
box.add_child(hp_label) box.add_child(hp_label)
(_player_panels if slot.side == 0 else _enemy_panels).add_child(panel) (_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 _panels[slot.id] = entry
_set_hp(entry, mon.pokemon.current_hp, mon.pokemon.get_max_hp(), false) _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: func animate_hp(slot_id : int, hp : int, max_hp : int) -> void:
var entry : Dictionary = _panels.get(slot_id, {}) var entry : Dictionary = _panels.get(slot_id, {})

View file

@ -77,6 +77,7 @@ func _start_camera(max_slots : int) -> void:
_camera.global_transform = _previous_camera.global_transform if _previous_camera else final_transform _camera.global_transform = _previous_camera.global_transform if _previous_camera else final_transform
if _previous_camera: _camera.fov = _previous_camera.fov if _previous_camera: _camera.fov = _previous_camera.fov
_camera.make_current() _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() 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) 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) ui.remove_slot_panel(event.source_slot)
await ui.show_message(event.describe(), 1.0) 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: BattleEvent.Type.TURN_START:
ui.hide_message() #pas de texte : les menus prennent la suite ui.hide_message() #pas de texte : les menus prennent la suite

View file

@ -1,7 +1,7 @@
class_name DamageCalculator class_name DamageCalculator
extends RefCounted 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 CRIT_MULTIPLIER : float = 1.5
const STAB_MULTIPLIER : float = 1.5 const STAB_MULTIPLIER : float = 1.5
const SPREAD_MULTIPLIER : float = 0.75 #capacite touchant plusieurs cibles 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 #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 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 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 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: static func estimate(attacker : BattleMon, defender : BattleMon, move : MoveData, spread : bool = false) -> int:
#degats attendus, sans hasard (utilise par l'IA) #degats attendus, sans hasard (utilise par l'IA)
return int(calculate(attacker, defender, move, null, spread, true)["damage"]) 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))

View file

@ -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

View file

@ -0,0 +1 @@
uid://cjhxvijo8je4c

View file

@ -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())

View file

@ -0,0 +1 @@
uid://drr1643p1u111

View file

@ -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

View file

@ -0,0 +1 @@
uid://dfleojc0ag7eq

View file

@ -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

View file

@ -0,0 +1 @@
uid://d3wtsfx3xjd15

View file

@ -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

View file

@ -0,0 +1 @@
uid://upmubyjayuqe

View file

@ -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

View file

@ -0,0 +1 @@
uid://bv4iyk174vat5

View file

@ -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())

View file

@ -0,0 +1 @@
uid://7dqdniguqb3n

View file

@ -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

View file

@ -0,0 +1 @@
uid://bf3jsbukymc6v

View file

@ -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 ""

View file

@ -0,0 +1 @@
uid://e6oixl35p3ta

View file

@ -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)

View file

@ -0,0 +1 @@
uid://cvr1yldqosrt2

View file

@ -4,3 +4,4 @@
[node name="Battle" type="Node" unique_id=520840168] [node name="Battle" type="Node" unique_id=520840168]
script = ExtResource("1_5bjgp") script = ExtResource("1_5bjgp")
mass_simulation_count = 100