Add abilities to battle system

This commit is contained in:
TheThomaas 2026-10-22 15:25:25 +02:00
parent 8ea4111149
commit 1251112e3c
41 changed files with 490 additions and 17 deletions

View file

@ -0,0 +1,57 @@
class_name AbilityRegistry
extends RefCounted
#associe un talent (nom d'API) a son effet. Un talent sans script est simplement sans effet
#(avertissement en debug, une seule fois). Pour en ajouter : cree une classe BattleEffect et declare-la ici.
static var _warned : Dictionary = {}
static func create(api_name : String) -> BattleEffect:
match api_name:
#entree en combat
"intimidate": return IntimidateAbility.new()
"drizzle": return OnEnterWeatherAbility.new(BattleField.Weather.RAIN)
"drought": return OnEnterWeatherAbility.new(BattleField.Weather.SUN)
"sand-stream": return OnEnterWeatherAbility.new(BattleField.Weather.SANDSTORM)
"snow-warning": return OnEnterWeatherAbility.new(BattleField.Weather.SNOW)
#vitesse selon la meteo
"swift-swim": return WeatherSpeedAbility.new(BattleField.Weather.RAIN)
"chlorophyll": return WeatherSpeedAbility.new(BattleField.Weather.SUN)
"sand-rush": return WeatherSpeedAbility.new(BattleField.Weather.SANDSTORM)
"slush-rush": return WeatherSpeedAbility.new(BattleField.Weather.SNOW)
#immunites et absorptions
"levitate": return TypeAbsorbAbility.new("ground", 0.0, "", true)
"water-absorb": return TypeAbsorbAbility.new("water", 0.25)
"volt-absorb": return TypeAbsorbAbility.new("electric", 0.25)
"lightning-rod": return TypeAbsorbAbility.new("electric", 0.0, "special-attack")
"storm-drain": return TypeAbsorbAbility.new("water", 0.0, "special-attack")
"sap-sipper": return TypeAbsorbAbility.new("grass", 0.0, "attack")
"motor-drive": return TypeAbsorbAbility.new("electric", 0.0, "speed")
"flash-fire": return FlashFireAbility.new()
#contact
"static": return ContactStatusAbility.new("paralysis")
"poison-point": return ContactStatusAbility.new("poison")
"flame-body": return ContactStatusAbility.new("burn")
"rough-skin": return RoughSkinAbility.new()
#degats et stats
"overgrow": return PinchBoostAbility.new("grass")
"blaze": return PinchBoostAbility.new("fire")
"torrent": return PinchBoostAbility.new("water")
"swarm": return PinchBoostAbility.new("bug")
"adaptability": return AdaptabilityAbility.new()
"technician": return TechnicianAbility.new()
"huge-power", "pure-power": return AttackBoostAbility.new(true)
"guts": return AttackBoostAbility.new(false)
"speed-boost": return SpeedBoostAbility.new()
_warn_missing(api_name)
return null
static func _warn_missing(api_name : String) -> void:
if !OS.is_debug_build() or _warned.has(api_name): return
_warned[api_name] = true
push_warning("[AbilityRegistry] talent sans script : '%s' (sans effet en combat)" % api_name)

View file

@ -0,0 +1 @@
uid://5bs0pyc6pr8w

View file

@ -0,0 +1,12 @@
class_name AdaptabilityAbility
extends BattleEffect
#Adaptabilite : le bonus de meme type (STAB) passe de x1.5 a x2
func _init() -> void:
id = &"adaptability"
func modify_outgoing_damage(attacker : BattleMon, _defender : BattleMon, move : MoveData, damage : float) -> float:
if attacker.get_types().has(move.type.to_lower()):
return damage * (2.0 / DamageCalculator.STAB_MULTIPLIER)
return damage

View file

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

View file

@ -0,0 +1,17 @@
class_name AttackBoostAbility
extends BattleEffect
#Force Pure / Cran : Attaque x2 (Force Pure, Coloforce) ou x1.5 avec un statut (Cran, qui annule aussi le malus de brulure)
var always_double : bool
func _init(p_always_double : bool) -> void:
id = &"attack_boost"
always_double = p_always_double
func modify_stat(mon : BattleMon, stat : PokemonNature.Stat, value : float) -> float:
if stat != PokemonNature.Stat.ATTACK: return value
if always_double: return value * 2.0
if mon.pokemon.status == PokemonInstance.Status.NONE: return value
var burn_undo : float = 2.0 if mon.pokemon.status == PokemonInstance.Status.BURN else 1.0
return value * 1.5 * burn_undo

View file

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

View file

@ -0,0 +1,24 @@
class_name ContactStatusAbility
extends BattleEffect
#Statik, Point Poison, Corps Ardent : chance d'infliger un statut a l'attaquant qui touche au contact.
#PokeAPI ne dit pas si une capacite fait contact : on considere que toutes les capacites physiques le font.
var ailment : String
var chance : int
func _init(p_ailment : String, p_chance : int = 30) -> void:
id = &"contact_status"
ailment = p_ailment
chance = p_chance
func on_after_hit(battle : Battle, attacker : BattleSlot, defender : BattleSlot, move : MoveData, damage : int) -> void:
if damage <= 0 or move.category != "physical" or attacker == defender or !attacker.is_active(): return
var status : int = StatusRegistry.from_ailment(ailment)
if attacker.mon.pokemon.status != PokemonInstance.Status.NONE: return
if StatusRegistry.is_immune(attacker.mon.get_types(), status): return
if battle.rng.randi_range(1, 100) > chance: return
await battle.announce_ability(defender)
await battle.inflict_status(defender, attacker, ailment, null, false)

View file

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

View file

@ -0,0 +1,19 @@
class_name FlashFireAbility
extends BattleEffect
#Torche : immunise contre le type Feu, puis renforce ses propres capacites Feu de 50 %
func _init() -> void:
id = &"flash_fire"
func on_before_hit(battle : Battle, attacker : BattleSlot, defender : BattleSlot, move : MoveData) -> bool:
if attacker == defender or move.type.to_lower() != "fire": return false
await battle.announce_ability(defender)
defender.mon.flags["flash_fire"] = true
await battle.message("La puissance Feu de %s augmente !" % defender.mon.display_name())
return true
func modify_outgoing_damage(attacker : BattleMon, _defender : BattleMon, move : MoveData, damage : float) -> float:
if attacker.flags.get("flash_fire", false) and move.type.to_lower() == "fire":
return damage * 1.5
return damage

View file

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

View file

@ -0,0 +1,14 @@
class_name IntimidateAbility
extends BattleEffect
#Intimidation : a l'entree en combat, baisse l'Attaque de tous les adversaires
func _init() -> void:
id = &"intimidate"
func on_enter(battle : Battle, slot : BattleSlot) -> void:
var targets : Array[BattleSlot] = battle.get_opponents(slot)
if targets.is_empty(): return
await battle.announce_ability(slot)
for target in targets:
await battle.change_stat(target, "attack", -1)

View file

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

View file

@ -0,0 +1,15 @@
class_name OnEnterWeatherAbility
extends BattleEffect
#Crachin, Sechesse, Sable Volant, Alerte Neige : change la meteo a l'entree en combat
var weather : BattleField.Weather
func _init(p_weather : BattleField.Weather) -> void:
id = &"weather_setter"
weather = p_weather
func on_enter(battle : Battle, slot : BattleSlot) -> void:
if battle.field.weather == weather: return
await battle.announce_ability(slot)
await battle.set_weather(weather, slot)

View file

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

View file

@ -0,0 +1,15 @@
class_name PinchBoostAbility
extends BattleEffect
#Engrais, Brasier, Torrent, Essaim : capacites d'un type x1.5 quand les PV sont a 1/3 ou moins
var boosted_type : String
func _init(p_type : String) -> void:
id = &"pinch_boost"
boosted_type = p_type
func modify_outgoing_damage(attacker : BattleMon, _defender : BattleMon, move : MoveData, damage : float) -> float:
if move.type.to_lower() != boosted_type: return damage
if attacker.pokemon.current_hp * 3 <= attacker.pokemon.get_max_hp(): return damage * 1.5
return damage

View file

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

View file

@ -0,0 +1,12 @@
class_name RoughSkinAbility
extends BattleEffect
#Peau Dure : l'attaquant au contact perd 1/8 de ses PV
func _init() -> void:
id = &"rough_skin"
func on_after_hit(battle : Battle, attacker : BattleSlot, defender : BattleSlot, move : MoveData, damage : int) -> void:
if damage <= 0 or move.category != "physical" or attacker == defender or !attacker.is_active(): return
await battle.announce_ability(defender)
await battle.hurt(attacker, maxi(1, floori(attacker.mon.pokemon.get_max_hp() / 8.0)), "%s est blessé !" % attacker.mon.display_name())

View file

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

View file

@ -0,0 +1,11 @@
class_name SpeedBoostAbility
extends BattleEffect
#Turbo : la Vitesse monte d'un cran a la fin de chaque tour
func _init() -> void:
id = &"speed_boost"
func on_turn_end(battle : Battle, slot : BattleSlot) -> void:
await battle.announce_ability(slot)
await battle.change_stat(slot, "speed", 1)

View file

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

View file

@ -0,0 +1,10 @@
class_name TechnicianAbility
extends BattleEffect
#Technicien : capacites de puissance 60 ou moins x1.5
func _init() -> void:
id = &"technician"
func modify_outgoing_damage(_attacker : BattleMon, _defender : BattleMon, move : MoveData, damage : float) -> float:
return damage * 1.5 if move.power > 0 and move.power <= 60 else damage

View file

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

View file

@ -0,0 +1,33 @@
class_name TypeAbsorbAbility
extends BattleEffect
#annule les capacites d'un type, avec un bonus eventuel :
# Absorb Eau / Absorb Volt : soigne 1/4 des PV ; Paratonnerre / Lavabo : Att. Spe. +1 ;
# Herbivore : Attaque +1 ; Motorisé : Vitesse +1 ; Levitation : aucun bonus (capacites de degats seulement)
var absorbed_type : String
var heal_fraction : float
var boost_stat : String
var damaging_only : bool
func _init(p_type : String, p_heal_fraction : float = 0.0, p_boost_stat : String = "", p_damaging_only : bool = false) -> void:
id = &"type_absorb"
absorbed_type = p_type
heal_fraction = p_heal_fraction
boost_stat = p_boost_stat
damaging_only = p_damaging_only
func on_before_hit(battle : Battle, attacker : BattleSlot, defender : BattleSlot, move : MoveData) -> bool:
if attacker == defender or move.type.to_lower() != absorbed_type: return false
if damaging_only and move.power <= 0: return false
await battle.announce_ability(defender)
var defender_name : String = defender.mon.display_name()
if heal_fraction > 0.0:
var healed : int = await battle.heal(defender, maxi(1, floori(defender.mon.pokemon.get_max_hp() * heal_fraction)), "%s récupère des PV !" % defender_name)
if healed == 0: await battle.message("Ça n'affecte pas %s..." % defender_name)
elif boost_stat != "":
await battle.change_stat(defender, boost_stat, 1)
else:
await battle.message("Ça n'affecte pas %s..." % defender_name)
return true

View file

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

View file

@ -0,0 +1,15 @@
class_name WeatherSpeedAbility
extends BattleEffect
#Glissade, Chlorophylle, Baigne Sable, Chasse-Neige : Vitesse x2 sous une meteo donnee
var weather : BattleField.Weather
func _init(p_weather : BattleField.Weather) -> void:
id = &"weather_speed"
weather = p_weather
func modify_stat(mon : BattleMon, stat : PokemonNature.Stat, value : float) -> float:
if stat == PokemonNature.Stat.SPEED and mon.field != null and mon.field.weather == weather:
return value * 2.0
return value

View file

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

View file

@ -28,6 +28,7 @@ var turn : int = 0
var max_turns : int = 0 #0 = illimite ; sinon le combat s'arrete en match nul apres ce nombre de tours (utile pour les tests)
var outcome : Outcome = Outcome.NONE
var slots : Array[BattleSlot] = []
var field : BattleField = BattleField.new() #meteo (terrains et barrieres plus tard)
func _init(p_setup : BattleSetup, p_presenter : BattlePresenter = null) -> void:
setup = p_setup
@ -46,7 +47,7 @@ func _build_slots() -> void:
slot.index = int(per_side_count.get(participant.side, 0))
per_side_count[participant.side] = slot.index + 1
slot.participant = participant
slot.set_pokemon(pokemon)
_place(slot, pokemon)
slots.append(slot)
#---------- boucle principale ----------
@ -55,6 +56,8 @@ func run() -> Outcome:
await emit_event(BattleEvent.make(BattleEvent.Type.BATTLE_START))
for slot in slots:
await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
for slot in _slots_by_speed():
await _run_on_enter(slot) #talents d'entree (Intimidation, Crachin...)
if check_end():
return await _finish()
@ -143,6 +146,21 @@ func heal(slot : BattleSlot, amount : int, text : String = "") -> int:
await emit_event(event)
return healed
## Annonce qu'un talent se declenche.
func announce_ability(slot : BattleSlot) -> void:
if slot.mon == null or slot.mon.pokemon.ability == null: return
await message("%s utilise son talent %s !" % [slot.mon.display_name(), slot.mon.pokemon.ability.get_display_name()])
## Change la meteo. Renvoie false si c'est deja la meteo en cours (la capacite ou le talent echoue).
func set_weather(weather : BattleField.Weather, _source : BattleSlot = null, turns : int = BattleField.DEFAULT_WEATHER_TURNS) -> bool:
if weather == BattleField.Weather.NONE or field.weather == weather: return false
field.set_weather(weather, turns)
var event := BattleEvent.make(BattleEvent.Type.WEATHER_CHANGED)
event.amount = weather
event.text = BattleField.start_message(weather)
await emit_event(event)
return true
## Tente d'infliger un statut (nom PokeAPI : "burn", "paralysis", "confusion"...). Renvoie true si applique.
## announce_failure : afficher pourquoi ca echoue (capacite de statut), sinon echec silencieux (effet secondaire).
func inflict_status(source : BattleSlot, target : BattleSlot, ailment : String, move : MoveData, announce_failure : bool) -> bool:
@ -267,28 +285,44 @@ func _execute_switch(action : BattleAction) -> void:
var slot : BattleSlot = action.slot
if !get_replacement_options(slot).has(action.switch_to): return
await emit_event(BattleEvent.make(BattleEvent.Type.WITHDRAWN, slot))
slot.set_pokemon(action.switch_to)
_place(slot, action.switch_to)
slot.mon.acted_this_turn = true
await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
await _run_on_enter(slot)
#---------- fin de tour ----------
func _end_of_turn() -> void:
#effets de fin de tour (brulure, poison...), du plus rapide au plus lent
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:
for slot in _slots_by_speed():
if !slot.is_active(): continue
for effect in slot.mon.get_effects():
if !slot.is_active(): break
await effect.on_turn_end(self, slot)
if check_end(): return
await _weather_end_of_turn()
for slot in slots:
if slot.mon != null: slot.mon.cleanup_volatile()
func _weather_end_of_turn() -> void:
if field.weather == BattleField.Weather.NONE: return
if field.weather == BattleField.Weather.SANDSTORM:
for slot in get_active_slots():
if BattleField.is_sandstorm_immune(slot.mon.get_types()): continue
await hurt(slot, maxi(1, floori(slot.mon.pokemon.get_max_hp() / 16.0)), "%s est frappé par la tempête de sable !" % slot.mon.display_name())
if check_end(): return
field.weather_turns -= 1
if field.weather_turns <= 0:
var ended : BattleField.Weather = field.weather
field.set_weather(BattleField.Weather.NONE)
var event := BattleEvent.make(BattleEvent.Type.WEATHER_CHANGED)
event.amount = BattleField.Weather.NONE
event.text = BattleField.end_message(ended)
await emit_event(event)
func _replace_fainted() -> void:
for slot in slots:
if slot.mon == null or !slot.mon.is_fainted(): continue
@ -300,8 +334,27 @@ 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)
_place(slot, chosen)
await emit_event(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
await _run_on_enter(slot)
#---------- placement et entree en combat ----------
func _place(slot : BattleSlot, pokemon : PokemonInstance) -> void:
slot.set_pokemon(pokemon)
slot.mon.field = field
func _run_on_enter(slot : BattleSlot) -> void:
if slot.mon == null: return
for effect in slot.mon.get_effects():
if !slot.is_active(): return
await effect.on_enter(self, slot)
func _slots_by_speed() -> Array[BattleSlot]:
var ordered : Array[BattleSlot] = get_active_slots()
ordered.sort_custom(func(a : BattleSlot, b : BattleSlot) -> bool:
return a.mon.get_stat(PokemonNature.Stat.SPEED) > b.mon.get_stat(PokemonNature.Stat.SPEED))
return ordered
#---------- requetes (utilisees par les controleurs, les effets et TargetResolver) ----------

View file

@ -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 _status_score(slot, move, targets)
if move.power <= 0 or move.category == "status": return _status_score(battle, slot, move, targets)
var accuracy : float = 1.0 if move.accuracy <= 0 else move.accuracy / 100.0
var spread : bool = targets.size() > 1
@ -155,10 +155,15 @@ func _score(battle : Battle, slot : BattleSlot, move : MoveData, candidate : Var
if damage >= remaining: score += KILL_SCORE
return score
func _status_score(slot : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> float:
func _status_score(battle : Battle, 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
var weather : int = BattleField.weather_for_move(move.api_name)
if weather != BattleField.Weather.NONE:
return 6.0 if battle.field.weather != weather else 0.1
for t in targets:
var enemy : bool = t.side != slot.side

View file

@ -184,7 +184,8 @@ func _seeded_team(rng : RandomNumberGenerator, ids : Array, size : int) -> Pokem
for i in size:
var species : PokemonData = Pokedex.get_pokemon(ids[rng.randi_range(0, ids.size() - 1)])
if species == null: continue
team.add_pokemon(PokemonInstance.create(species, rng.randi_range(level_min, level_max), rng.randi_range(0, 24)))
var ability : AbilityData = PokemonInstance.pick_default_ability(species, rng)
team.add_pokemon(PokemonInstance.create(species, rng.randi_range(level_min, level_max), rng.randi_range(0, 24), null, ability))
return team
func _describe(setup : BattleSetup) -> String:

View file

@ -10,7 +10,7 @@ enum Type {
MOVE_USED, MOVE_MISSED, DAMAGE, CRITICAL,
SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT,
FAINTED, FLED, MESSAGE,
STATUS_CHANGED, HEAL,
STATUS_CHANGED, HEAL, WEATHER_CHANGED,
}
var type : Type = Type.MESSAGE
@ -52,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, Type.STATUS_CHANGED, Type.HEAL: return text
Type.MESSAGE, Type.STATUS_CHANGED, Type.HEAL, Type.WEATHER_CHANGED: return text
return ""

View file

@ -0,0 +1,66 @@
class_name BattleField
extends RefCounted
#etat du terrain, commun aux deux camps : meteo (terrains et barrieres viendront plus tard).
#les talents (Crachin, Sechesse...) et les capacites (Danse Pluie...) la modifient via Battle.set_weather().
enum Weather { NONE, SUN, RAIN, SANDSTORM, SNOW }
const DEFAULT_WEATHER_TURNS : int = 5
#capacites qui changent la meteo (nom d'API)
const WEATHER_MOVES : Dictionary = {
"sunny-day": Weather.SUN,
"rain-dance": Weather.RAIN,
"sandstorm": Weather.SANDSTORM,
"hail": Weather.SNOW,
"snowscape": Weather.SNOW,
}
var weather : Weather = Weather.NONE
var weather_turns : int = 0
static func weather_for_move(move_api_name : String) -> int:
return int(WEATHER_MOVES.get(move_api_name, Weather.NONE))
func set_weather(new_weather : Weather, turns : int = DEFAULT_WEATHER_TURNS) -> void:
weather = new_weather
weather_turns = turns if new_weather != Weather.NONE else 0
## Multiplicateur de degats selon la meteo et le type de la capacite.
func damage_multiplier(move_type : String) -> float:
match weather:
Weather.SUN:
if move_type == "fire": return 1.5
if move_type == "water": return 0.5
Weather.RAIN:
if move_type == "water": return 1.5
if move_type == "fire": return 0.5
return 1.0
## Bonus defensifs de la meteo : tempete de sable (Roche, Def. Spe. x1.5), neige (Glace, Defense x1.5).
func modify_stat(mon : BattleMon, stat : PokemonNature.Stat, value : float) -> float:
if weather == Weather.SANDSTORM and stat == PokemonNature.Stat.SP_DEFENSE and mon.get_types().has("rock"):
return value * 1.5
if weather == Weather.SNOW and stat == PokemonNature.Stat.DEFENSE and mon.get_types().has("ice"):
return value * 1.5
return value
static func is_sandstorm_immune(types : Array[String]) -> bool:
return types.has("rock") or types.has("ground") or types.has("steel")
static func start_message(w : int) -> String:
match w:
Weather.SUN: return "Les rayons du soleil brillent !"
Weather.RAIN: return "Il commence à pleuvoir !"
Weather.SANDSTORM: return "Une tempête de sable se lève !"
Weather.SNOW: return "Il commence à neiger !"
return ""
static func end_message(w : int) -> String:
match w:
Weather.SUN: return "Le soleil se couche."
Weather.RAIN: return "La pluie s'arrête."
Weather.SANDSTORM: return "La tempête de sable se calme."
Weather.SNOW: return "La neige s'arrête."
return ""

View file

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

View file

@ -36,11 +36,16 @@ 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
var field : BattleField = null #terrain du combat (meteo...), renseigne par Battle
var flags : Dictionary = {} #petits etats libres pour les talents (ex. "flash_fire")
var _ability_effect : BattleEffect = null
func _init(p_pokemon : PokemonInstance) -> void:
pokemon = p_pokemon
if pokemon.status == PokemonInstance.Status.SLEEP:
status_turns = 2 #deja endormi en entrant en combat
if pokemon.ability != null:
_ability_effect = AbilityRegistry.create(pokemon.ability.api_name)
func display_name() -> String:
return pokemon.get_display_name()
@ -61,6 +66,7 @@ func get_effects() -> Array[BattleEffect]:
var result : Array[BattleEffect] = []
var status_effect : BattleEffect = StatusRegistry.effect_for(int(pokemon.status))
if status_effect != null: result.append(status_effect)
if _ability_effect != null: result.append(_ability_effect)
result.append_array(volatile)
return result
@ -106,6 +112,7 @@ func get_stat(stat : PokemonNature.Stat) -> float:
var value : float = pokemon.get_stat(stat) * _stage_multiplier(get_stage(stat))
for effect in get_effects():
value = effect.modify_stat(self, stat, value)
if field != null: value = field.modify_stat(self, stat, value)
return value
## Multiplicateur de precision de cette attaque contre une cible (boost de precision - esquive).

View file

@ -28,6 +28,9 @@ static func calculate(attacker : BattleMon, defender : BattleMon, move : MoveDat
var stab : float = STAB_MULTIPLIER if attacker.get_types().has(move.type.to_lower()) else 1.0
damage *= roll * stab * effectiveness
if attacker.field != null: damage *= attacker.field.damage_multiplier(move.type.to_lower())
for effect in attacker.get_effects():
damage = effect.modify_outgoing_damage(attacker, defender, move, damage)
if critical: damage *= CRIT_MULTIPLIER
if spread: damage *= SPREAD_MULTIPLIER

View file

@ -15,6 +15,22 @@ func modify_stat(_mon : BattleMon, _stat : PokemonNature.Stat, value : float) ->
func before_action(_battle : Battle, _slot : BattleSlot, _action : BattleAction) -> bool:
return true
## Appele quand le pokemon entre en combat (Intimidation, Crachin...).
func on_enter(_battle : Battle, _slot : BattleSlot) -> void:
pass
## Appele sur le DEFENSEUR avant qu'une capacite ne le touche. Renvoyer true = la capacite est annulee (talent d'absorption...).
func on_before_hit(_battle : Battle, _attacker : BattleSlot, _defender : BattleSlot, _move : MoveData) -> bool:
return false
## Appele sur le DEFENSEUR apres chaque coup recu (talents de contact...).
func on_after_hit(_battle : Battle, _attacker : BattleSlot, _defender : BattleSlot, _move : MoveData, _damage : int) -> void:
pass
## Appele sur l'ATTAQUANT pendant le calcul des degats : renvoie les degats modifies (Technicien, Adaptabilite...).
func modify_outgoing_damage(_attacker : BattleMon, _defender : BattleMon, _move : MoveData, damage : float) -> float:
return damage
## Appele a la fin de chaque tour (degats de brulure, de poison...).
func on_turn_end(_battle : Battle, _slot : BattleSlot) -> void:
pass

View file

@ -52,6 +52,7 @@ static func _use_damaging(battle : Battle, actor : BattleSlot, move : MoveData,
if !_hits(battle, actor, target, move):
await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
continue
if await _blocked_before_hit(battle, actor, target, move): continue
var hits : int = _roll_hit_count(battle, move)
var landed : int = 0
@ -61,8 +62,10 @@ static func _use_damaging(battle : Battle, actor : BattleSlot, move : MoveData,
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"]))
var dealt : int = await battle.deal_damage(actor, target, int(calc["damage"]))
total_dealt += dealt
landed += 1
await _after_hit(battle, actor, target, move, dealt)
if calc["critical"]: await battle.emit_event(BattleEvent.make(BattleEvent.Type.CRITICAL, actor, target))
if landed == 0:
@ -127,6 +130,12 @@ static func _secondary_effects(battle : Battle, actor : BattleSlot, target : Bat
#---------- capacites de statut ----------
static func _use_status(battle : Battle, actor : BattleSlot, move : MoveData, targets : Array[BattleSlot]) -> void:
#capacites de meteo : une seule fois, quelles que soient les cibles
var weather : int = BattleField.weather_for_move(move.api_name)
if weather != BattleField.Weather.NONE:
if !await battle.set_weather(weather, actor): await battle.message("Mais cela échoue !")
return
var handled : bool = false
for target in targets:
@ -134,6 +143,7 @@ static func _use_status(battle : Battle, actor : BattleSlot, move : MoveData, ta
if target != actor and !_hits(battle, actor, target, move):
await battle.emit_event(BattleEvent.make(BattleEvent.Type.MOVE_MISSED, actor, target))
continue
if target != actor and await _blocked_before_hit(battle, actor, target, move): continue
if move.ailment != "none":
handled = true
@ -157,6 +167,19 @@ static func _use_status(battle : Battle, actor : BattleSlot, move : MoveData, ta
if !handled:
await battle.message("Mais cela n'a pas d'effet pour l'instant.")
#---------- crochets des talents ----------
static func _blocked_before_hit(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> bool:
#un talent du defenseur peut annuler la capacite (Absorb Eau, Levitation...)
for effect in target.mon.get_effects():
if await effect.on_before_hit(battle, actor, target, move): return true
return false
static func _after_hit(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData, damage : int) -> void:
#talents de contact du defenseur (Statik, Peau Dure...)
for effect in target.mon.get_effects():
await effect.on_after_hit(battle, actor, target, move, damage)
#---------- precision et coups multiples ----------
static func _hits(battle : Battle, actor : BattleSlot, target : BattleSlot, move : MoveData) -> bool:

View file

@ -15,6 +15,7 @@ const MAX_LEVEL : int = 100
@export var nature : PokemonNature.Nature = PokemonNature.Nature.HARDY
@export var current_hp : int = 1
@export var status : Status = Status.NONE
@export var ability : AbilityData = null #talent (null = aucun)
@export_group("Capacités")
@export var learned_moves : Array[MoveData] = [] #toutes les capacites debloquees
@ -25,16 +26,29 @@ const MAX_LEVEL : int = 100
#---------- creation ----------
static func create(p_species : PokemonData, p_level : int, p_nature : int = -1, p_mount_data : MountProfile = null) -> PokemonInstance:
static func create(p_species : PokemonData, p_level : int, p_nature : int = -1, p_mount_data : MountProfile = null, p_ability : AbilityData = null) -> PokemonInstance:
var p := PokemonInstance.new()
p.species = p_species
p.level = clampi(p_level, 1, MAX_LEVEL)
p.nature = (p_nature as PokemonNature.Nature) if p_nature >= 0 else PokemonNature.random_nature()
p.mount_data = p_mount_data
p.ability = p_ability if p_ability != null else pick_default_ability(p_species)
p.fill_moves_for_level()
p.current_hp = p.get_max_hp()
return p
## Talent par defaut d'une espece : tire au hasard parmi les talents normaux (le talent cache seulement s'il n'y en a pas d'autre).
## rng : generateur a utiliser pour un resultat reproductible (tests) ; sinon hasard global.
static func pick_default_ability(p_species : PokemonData, rng : RandomNumberGenerator = null) -> AbilityData:
var normal : Array[AbilityData] = []
var hidden : Array[AbilityData] = []
for entry in p_species.abilities:
if entry == null or entry.ability == null: continue
(hidden if entry.is_hidden else normal).append(entry.ability)
var pool : Array[AbilityData] = normal if !normal.is_empty() else hidden
if pool.is_empty(): return null
return pool[rng.randi_range(0, pool.size() - 1)] if rng != null else pool.pick_random()
#---------- infos ----------
func get_display_name() -> String:
@ -172,6 +186,7 @@ func to_dict() -> Dictionary:
"learned_moves": learned,
"active_moves": slots,
"mount_data": mount_data.resource_path if mount_data else "",
"ability": ability.api_name if ability else "",
}
static func from_dict(data : Dictionary) -> PokemonInstance:
@ -200,5 +215,8 @@ static func from_dict(data : Dictionary) -> PokemonInstance:
if mount_path.begins_with("res://") and ResourceLoader.exists(mount_path):
p.mount_data = load(mount_path) as MountProfile
var ability_name : String = data.get("ability", "")
p.ability = Pokedex.get_ability(ability_name) if ability_name != "" else pick_default_ability(species_data)
p.current_hp = clampi(int(data.get("hp", p.get_max_hp())), 0, p.get_max_hp())
return p

View file

@ -11,6 +11,10 @@ extends Resource
@export var random_nature : bool = true
@export var nature : PokemonNature.Nature = PokemonNature.Nature.HARDY
@export_group("Talent")
## Vide = talent tire au hasard parmi les talents normaux de l'espece.
@export var ability : AbilityData
@export_group("Capacités")
## Vide = capacites automatiques selon le niveau. Sinon : ces capacites (max 4) sont equipees.
@export var forced_moves : Array[MoveData] = []
@ -18,8 +22,8 @@ extends Resource
func create_instance() -> PokemonInstance:
if species == null: return null
var nature_value : int = -1 if random_nature else int(nature)
var p := PokemonInstance.create(species, level, nature_value)
var p := PokemonInstance.create(species, level, nature_value, null, ability)
p.nickname = nickname
if !forced_moves.is_empty():
p.set_active_moves(forced_moves)
return p
return p

View file

@ -324,6 +324,7 @@ func _update_info() -> void:
lines.append("%s Niv.%d" % [p.get_display_name(), p.level])
lines.append("PV %d / %d" % [p.current_hp, p.get_max_hp()])
lines.append("Nature : %s" % String(PokemonNature.Nature.keys()[p.nature]).capitalize())
if p.ability != null: lines.append("Talent : %s" % p.ability.get_display_name())
if p.can_be_mounted(): lines.append("Peut être monté")
lines.append("")
for slot in p.active_moves:

View file

@ -231,6 +231,7 @@ func _update_info(index : int) -> void:
var lines : Array[String] = []
lines.append("%s Niv.%d" % [p.get_display_name(), p.level])
lines.append("Nature : %s" % String(PokemonNature.Nature.keys()[p.nature]).capitalize())
if p.ability != null: lines.append("Talent : %s" % p.ability.get_display_name())
if p.can_be_mounted(): lines.append("Peut être monté")
lines.append("")
lines.append("PV : %d / %d" % [p.current_hp, p.get_max_hp()])

View file

@ -55,7 +55,11 @@ albedo_texture = ExtResource("5_xly4w")
[sub_resource type="Resource" id="Resource_yebv1"]
script = ExtResource("8_tfb7h")
species = ExtResource("10_htcyl")
level = null
nickname = "Testkatchu"
random_nature = null
nature = null
forced_moves = null
metadata/_custom_type_script = "uid://cqsokrfi2d05y"
[sub_resource type="Resource" id="Resource_h63fm"]
@ -73,6 +77,9 @@ albedo_color = Color(1, 1, 1, 0)
script = ExtResource("8_tfb7h")
species = ExtResource("14_2rm86")
level = 13
nickname = null
random_nature = null
nature = null
forced_moves = Array[ExtResource("9_2rm86")]([ExtResource("11_g0lob"), ExtResource("12_08mbr"), ExtResource("13_tfb7h")])
metadata/_custom_type_script = "uid://cqsokrfi2d05y"