Add effects to battle system
This commit is contained in:
parent
3c91f404c6
commit
e666c8a34d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, {})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
24
game/battle/effects/battle_effect.gd
Normal file
24
game/battle/effects/battle_effect.gd
Normal 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
|
||||
1
game/battle/effects/battle_effect.gd.uid
Normal file
1
game/battle/effects/battle_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cjhxvijo8je4c
|
||||
12
game/battle/effects/burn_effect.gd
Normal file
12
game/battle/effects/burn_effect.gd
Normal 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())
|
||||
1
game/battle/effects/burn_effect.gd.uid
Normal file
1
game/battle/effects/burn_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://drr1643p1u111
|
||||
27
game/battle/effects/confusion_effect.gd
Normal file
27
game/battle/effects/confusion_effect.gd
Normal 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
|
||||
1
game/battle/effects/confusion_effect.gd.uid
Normal file
1
game/battle/effects/confusion_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dfleojc0ag7eq
|
||||
14
game/battle/effects/flinch_effect.gd
Normal file
14
game/battle/effects/flinch_effect.gd
Normal 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
|
||||
1
game/battle/effects/flinch_effect.gd.uid
Normal file
1
game/battle/effects/flinch_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d3wtsfx3xjd15
|
||||
17
game/battle/effects/freeze_effect.gd
Normal file
17
game/battle/effects/freeze_effect.gd
Normal 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
|
||||
1
game/battle/effects/freeze_effect.gd.uid
Normal file
1
game/battle/effects/freeze_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://upmubyjayuqe
|
||||
16
game/battle/effects/paralysis_effect.gd
Normal file
16
game/battle/effects/paralysis_effect.gd
Normal 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
|
||||
1
game/battle/effects/paralysis_effect.gd.uid
Normal file
1
game/battle/effects/paralysis_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bv4iyk174vat5
|
||||
21
game/battle/effects/poison_effect.gd
Normal file
21
game/battle/effects/poison_effect.gd
Normal 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())
|
||||
1
game/battle/effects/poison_effect.gd.uid
Normal file
1
game/battle/effects/poison_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://7dqdniguqb3n
|
||||
16
game/battle/effects/sleep_effect.gd
Normal file
16
game/battle/effects/sleep_effect.gd
Normal 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
|
||||
1
game/battle/effects/sleep_effect.gd.uid
Normal file
1
game/battle/effects/sleep_effect.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bf3jsbukymc6v
|
||||
49
game/battle/effects/status_registry.gd
Normal file
49
game/battle/effects/status_registry.gd
Normal 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 ""
|
||||
1
game/battle/effects/status_registry.gd.uid
Normal file
1
game/battle/effects/status_registry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://e6oixl35p3ta
|
||||
175
game/battle/move_executor.gd
Normal file
175
game/battle/move_executor.gd
Normal 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)
|
||||
1
game/battle/move_executor.gd.uid
Normal file
1
game/battle/move_executor.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cvr1yldqosrt2
|
||||
|
|
@ -4,3 +4,4 @@
|
|||
|
||||
[node name="Battle" type="Node" unique_id=520840168]
|
||||
script = ExtResource("1_5bjgp")
|
||||
mass_simulation_count = 100
|
||||
|
|
|
|||
Loading…
Reference in a new issue