Start work on battle system
This commit is contained in:
parent
0714a303c7
commit
9a3c383b26
297
game/battle/battle.gd
Normal file
297
game/battle/battle.gd
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
class_name Battle
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#moteur de combat au tour par tour, sans aucun visuel.
|
||||||
|
#gere n emplacements par camp (1v1, 2v2, 1 contre plusieurs, plusieurs contre 1).
|
||||||
|
#
|
||||||
|
#usage :
|
||||||
|
# var battle := Battle.new(setup, mon_presenter)
|
||||||
|
# var outcome : Battle.Outcome = await battle.run()
|
||||||
|
#
|
||||||
|
#a chaque evenement, la presentation est appelee et le combat attend qu'elle ait fini.
|
||||||
|
|
||||||
|
signal event_emitted(event : BattleEvent)
|
||||||
|
|
||||||
|
enum Outcome { NONE, PLAYER_WON, PLAYER_LOST, FLED }
|
||||||
|
|
||||||
|
const PLAYER_SIDE : int = 0
|
||||||
|
|
||||||
|
static var _struggle : MoveData = null
|
||||||
|
|
||||||
|
var setup : BattleSetup
|
||||||
|
var presenter : BattlePresenter
|
||||||
|
var rng : RandomNumberGenerator = RandomNumberGenerator.new()
|
||||||
|
var turn : int = 0
|
||||||
|
var outcome : Outcome = Outcome.NONE
|
||||||
|
var slots : Array[BattleSlot] = []
|
||||||
|
|
||||||
|
func _init(p_setup : BattleSetup, p_presenter : BattlePresenter = null) -> void:
|
||||||
|
setup = p_setup
|
||||||
|
presenter = p_presenter
|
||||||
|
if setup.seed_value != 0: rng.seed = setup.seed_value
|
||||||
|
else: rng.randomize()
|
||||||
|
_build_slots()
|
||||||
|
|
||||||
|
func _build_slots() -> void:
|
||||||
|
var per_side_count : Dictionary = {}
|
||||||
|
for participant in setup.participants:
|
||||||
|
for pokemon in participant.pick_starters():
|
||||||
|
var slot := BattleSlot.new()
|
||||||
|
slot.id = slots.size()
|
||||||
|
slot.side = participant.side
|
||||||
|
slot.index = int(per_side_count.get(participant.side, 0))
|
||||||
|
per_side_count[participant.side] = slot.index + 1
|
||||||
|
slot.participant = participant
|
||||||
|
slot.set_pokemon(pokemon)
|
||||||
|
slots.append(slot)
|
||||||
|
|
||||||
|
#---------- boucle principale ----------
|
||||||
|
|
||||||
|
func run() -> Outcome:
|
||||||
|
await _emit(BattleEvent.make(BattleEvent.Type.BATTLE_START))
|
||||||
|
for slot in slots:
|
||||||
|
await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
|
||||||
|
|
||||||
|
if _check_end():
|
||||||
|
return await _finish()
|
||||||
|
|
||||||
|
while outcome == Outcome.NONE:
|
||||||
|
turn += 1
|
||||||
|
var turn_event := BattleEvent.make(BattleEvent.Type.TURN_START)
|
||||||
|
turn_event.amount = turn
|
||||||
|
await _emit(turn_event)
|
||||||
|
|
||||||
|
var actions : Array[BattleAction] = await _collect_actions()
|
||||||
|
_sort_actions(actions)
|
||||||
|
for action in actions:
|
||||||
|
await _execute(action)
|
||||||
|
if _check_end(): break
|
||||||
|
if outcome != Outcome.NONE: break
|
||||||
|
|
||||||
|
await _replace_fainted()
|
||||||
|
if _check_end(): break
|
||||||
|
|
||||||
|
return await _finish()
|
||||||
|
|
||||||
|
func _finish() -> Outcome:
|
||||||
|
var end_event := BattleEvent.make(BattleEvent.Type.BATTLE_END)
|
||||||
|
match outcome:
|
||||||
|
Outcome.PLAYER_WON: end_event.text = "Victoire !"
|
||||||
|
Outcome.PLAYER_LOST: end_event.text = "Défaite..."
|
||||||
|
Outcome.FLED: end_event.text = "Vous avez pris la fuite."
|
||||||
|
await _emit(end_event)
|
||||||
|
return outcome
|
||||||
|
|
||||||
|
func _emit(event : BattleEvent) -> void:
|
||||||
|
event_emitted.emit(event)
|
||||||
|
if presenter != null:
|
||||||
|
await presenter.present(event)
|
||||||
|
|
||||||
|
func _msg(text : String) -> void:
|
||||||
|
var e := BattleEvent.make(BattleEvent.Type.MESSAGE)
|
||||||
|
e.text = text
|
||||||
|
await _emit(e)
|
||||||
|
|
||||||
|
#---------- fin de combat ----------
|
||||||
|
|
||||||
|
func _side_alive(side : int) -> bool:
|
||||||
|
for participant in setup.participants:
|
||||||
|
if participant.side == side and participant.has_able_pokemon(): return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _check_end() -> bool:
|
||||||
|
if outcome != Outcome.NONE: return true
|
||||||
|
var player_alive : bool = _side_alive(PLAYER_SIDE)
|
||||||
|
var enemy_alive : bool = _side_alive(1 - PLAYER_SIDE)
|
||||||
|
if player_alive and enemy_alive: return false
|
||||||
|
outcome = Outcome.PLAYER_WON if player_alive else Outcome.PLAYER_LOST
|
||||||
|
return true
|
||||||
|
|
||||||
|
#---------- actions du tour ----------
|
||||||
|
|
||||||
|
func _collect_actions() -> Array[BattleAction]:
|
||||||
|
var actions : Array[BattleAction] = []
|
||||||
|
for slot in slots:
|
||||||
|
if !slot.is_active(): continue
|
||||||
|
var action : BattleAction = await slot.participant.controller.choose_action(self, slot)
|
||||||
|
if action == null: action = BattleAction.use_struggle(slot) #securite : un controleur qui ne decide rien
|
||||||
|
actions.append(action)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
func _sort_actions(actions : Array[BattleAction]) -> void:
|
||||||
|
for a in actions:
|
||||||
|
match a.type:
|
||||||
|
BattleAction.Type.FLEE: a.priority = 8
|
||||||
|
BattleAction.Type.SWITCH: a.priority = 7
|
||||||
|
BattleAction.Type.ITEM: a.priority = 6
|
||||||
|
_: a.priority = a.move.priority if a.move else 0
|
||||||
|
a.speed = a.slot.mon.get_stat(PokemonNature.Stat.SPEED)
|
||||||
|
a.tiebreak = rng.randf()
|
||||||
|
|
||||||
|
actions.sort_custom(func(a : BattleAction, b : BattleAction) -> bool:
|
||||||
|
if a.priority != b.priority: return a.priority > b.priority
|
||||||
|
if a.speed != b.speed: return a.speed > b.speed
|
||||||
|
return a.tiebreak > b.tiebreak)
|
||||||
|
|
||||||
|
func _execute(action : BattleAction) -> void:
|
||||||
|
if !action.slot.is_active(): return #mis K.O. avant son tour
|
||||||
|
match action.type:
|
||||||
|
BattleAction.Type.MOVE: await _execute_move(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))
|
||||||
|
else:
|
||||||
|
await _msg("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))
|
||||||
|
slot.set_pokemon(action.switch_to)
|
||||||
|
await _emit(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))
|
||||||
|
|
||||||
|
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 _replace_fainted() -> void:
|
||||||
|
for slot in slots:
|
||||||
|
if slot.mon == null or !slot.mon.is_fainted(): continue
|
||||||
|
|
||||||
|
var options : Array[PokemonInstance] = get_replacement_options(slot)
|
||||||
|
if options.is_empty():
|
||||||
|
slot.clear() #plus de remplacant : l'emplacement reste vide
|
||||||
|
continue
|
||||||
|
|
||||||
|
var chosen : PokemonInstance = await slot.participant.controller.choose_replacement(self, slot)
|
||||||
|
if chosen == null or !options.has(chosen): chosen = options[0]
|
||||||
|
slot.set_pokemon(chosen)
|
||||||
|
await _emit(BattleEvent.make(BattleEvent.Type.SENT_OUT, slot))
|
||||||
|
|
||||||
|
#---------- requetes (utilisees par les controleurs et TargetResolver) ----------
|
||||||
|
|
||||||
|
func get_slot(id : int) -> BattleSlot:
|
||||||
|
return slots[id] if id >= 0 and id < slots.size() else null
|
||||||
|
|
||||||
|
func get_active_slots() -> Array[BattleSlot]:
|
||||||
|
var result : Array[BattleSlot] = []
|
||||||
|
for s in slots:
|
||||||
|
if s.is_active(): result.append(s)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_opponents(slot : BattleSlot) -> Array[BattleSlot]:
|
||||||
|
var result : Array[BattleSlot] = []
|
||||||
|
for s in slots:
|
||||||
|
if s.is_active() and s.side != slot.side: result.append(s)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_allies(slot : BattleSlot) -> Array[BattleSlot]:
|
||||||
|
var result : Array[BattleSlot] = []
|
||||||
|
for s in slots:
|
||||||
|
if s.is_active() and s.side == slot.side and s != slot: result.append(s)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_usable_moves(mon : BattleMon) -> Array[MoveSlot]:
|
||||||
|
var result : Array[MoveSlot] = []
|
||||||
|
for ms in mon.pokemon.active_moves:
|
||||||
|
if ms.has_pp(): result.append(ms)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_replacement_options(slot : BattleSlot) -> Array[PokemonInstance]:
|
||||||
|
#pokemon aptes de l'equipe du participant qui ne sont pas deja en jeu
|
||||||
|
var result : Array[PokemonInstance] = []
|
||||||
|
for p in slot.participant.team.members:
|
||||||
|
if p.is_fainted() or _is_in_play(p): continue
|
||||||
|
result.append(p)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _is_in_play(pokemon : PokemonInstance) -> bool:
|
||||||
|
for s in slots:
|
||||||
|
if s.mon != null and s.mon.pokemon == pokemon: return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
#---------- Lutte ----------
|
||||||
|
|
||||||
|
static func make_struggle() -> MoveData:
|
||||||
|
if _struggle == null:
|
||||||
|
_struggle = MoveData.new()
|
||||||
|
_struggle.api_name = "struggle"
|
||||||
|
_struggle.names = {"fr": "Lutte", "en": "Struggle"}
|
||||||
|
_struggle.type = "typeless"
|
||||||
|
_struggle.category = "physical"
|
||||||
|
_struggle.power = 50
|
||||||
|
_struggle.accuracy = 0
|
||||||
|
_struggle.pp = 1
|
||||||
|
_struggle.priority = 0
|
||||||
|
_struggle.target = "random-opponent"
|
||||||
|
return _struggle
|
||||||
1
game/battle/battle.gd.uid
Normal file
1
game/battle/battle.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://jqtd5v08rwtv
|
||||||
48
game/battle/battle_action.gd
Normal file
48
game/battle/battle_action.gd
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
class_name BattleAction
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#ce qu'un emplacement choisit de faire pendant un tour
|
||||||
|
|
||||||
|
enum Type { MOVE, SWITCH, ITEM, FLEE }
|
||||||
|
|
||||||
|
var type : Type = Type.MOVE
|
||||||
|
var slot : BattleSlot #qui agit
|
||||||
|
var move : MoveData
|
||||||
|
var move_slot : MoveSlot #null pour Lutte
|
||||||
|
var chosen_target : BattleSlot = null #cible choisie (null = automatique)
|
||||||
|
var switch_to : PokemonInstance
|
||||||
|
|
||||||
|
#calcules par Battle au moment de trier les actions
|
||||||
|
var priority : int = 0
|
||||||
|
var speed : float = 0.0
|
||||||
|
var tiebreak : float = 0.0
|
||||||
|
|
||||||
|
static func use_move(p_slot : BattleSlot, p_move_slot : MoveSlot, p_target : BattleSlot = null) -> BattleAction:
|
||||||
|
var a := BattleAction.new()
|
||||||
|
a.type = Type.MOVE
|
||||||
|
a.slot = p_slot
|
||||||
|
a.move_slot = p_move_slot
|
||||||
|
a.move = p_move_slot.move
|
||||||
|
a.chosen_target = p_target
|
||||||
|
return a
|
||||||
|
|
||||||
|
static func use_struggle(p_slot : BattleSlot) -> BattleAction:
|
||||||
|
#plus aucune capacite utilisable : Lutte
|
||||||
|
var a := BattleAction.new()
|
||||||
|
a.type = Type.MOVE
|
||||||
|
a.slot = p_slot
|
||||||
|
a.move = Battle.make_struggle()
|
||||||
|
return a
|
||||||
|
|
||||||
|
static func switch_pokemon(p_slot : BattleSlot, p_pokemon : PokemonInstance) -> BattleAction:
|
||||||
|
var a := BattleAction.new()
|
||||||
|
a.type = Type.SWITCH
|
||||||
|
a.slot = p_slot
|
||||||
|
a.switch_to = p_pokemon
|
||||||
|
return a
|
||||||
|
|
||||||
|
static func flee(p_slot : BattleSlot) -> BattleAction:
|
||||||
|
var a := BattleAction.new()
|
||||||
|
a.type = Type.FLEE
|
||||||
|
a.slot = p_slot
|
||||||
|
return a
|
||||||
1
game/battle/battle_action.gd.uid
Normal file
1
game/battle/battle_action.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://hispkfk8qdt6
|
||||||
143
game/battle/battle_ai.gd
Normal file
143
game/battle/battle_ai.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
class_name BattleAI
|
||||||
|
extends BattleController
|
||||||
|
|
||||||
|
#IA simple :
|
||||||
|
#- attaque : choisit la capacite et la cible qui font le plus de degats attendus
|
||||||
|
# (achever un adversaire est favorise, taper un allie est penalise)
|
||||||
|
#- changement volontaire : si son pokemon est tres desavantage (types) et a peu de PV, elle peut
|
||||||
|
# le remplacer par un pokemon du banc qui a un meilleur rapport de types face aux adversaires
|
||||||
|
|
||||||
|
var randomness : float = 0.15 #chance de jouer un coup au hasard, pour ne pas etre previsible
|
||||||
|
|
||||||
|
var can_switch : bool = true #false : ne change jamais volontairement (ex. un pokemon sauvage n'a de toute facon pas de banc)
|
||||||
|
var switch_chance : float = 0.6 #quand les conditions sont reunies, probabilite de vraiment changer
|
||||||
|
var hp_threshold : float = 0.4 #ne change que si ses PV sont sous ce ratio
|
||||||
|
var matchup_threshold : float = -1.0 #ne change que si le rapport de types est pire que cette valeur
|
||||||
|
var min_improvement : float = 1.0 #le remplacant doit etre meilleur d'au moins cette valeur
|
||||||
|
var min_replacement_hp : float = 0.3 #ne fait pas entrer un pokemon quasi K.O.
|
||||||
|
|
||||||
|
const KILL_SCORE : float = 20.0 #bonus de score d'une attaque qui acheve un adversaire (cf. _score)
|
||||||
|
|
||||||
|
var _claimed : Array[PokemonInstance] = [] #remplacants deja choisis ce tour (evite 2 emplacements sur le meme)
|
||||||
|
var _claimed_turn : int = -1
|
||||||
|
|
||||||
|
func choose_action(battle : Battle, slot : BattleSlot) -> BattleAction:
|
||||||
|
if _claimed_turn != battle.turn:
|
||||||
|
_claimed.clear()
|
||||||
|
_claimed_turn = battle.turn
|
||||||
|
|
||||||
|
var usable : Array[MoveSlot] = battle.get_usable_moves(slot.mon)
|
||||||
|
|
||||||
|
#options d'attaque = [MoveSlot, cible (BattleSlot ou null), score]
|
||||||
|
var options : Array = []
|
||||||
|
for move_slot in usable:
|
||||||
|
var move : MoveData = move_slot.move
|
||||||
|
var candidates : Array = [null]
|
||||||
|
if TargetResolver.needs_selection(battle, slot, move):
|
||||||
|
candidates = TargetResolver.get_selectable_targets(battle, slot, move)
|
||||||
|
for candidate in candidates:
|
||||||
|
options.append([move_slot, candidate, _score(battle, slot, move, candidate) + battle.rng.randf() * 0.01])
|
||||||
|
|
||||||
|
var best : Array = []
|
||||||
|
for option in options:
|
||||||
|
if best.is_empty() or option[2] > best[2]: best = option
|
||||||
|
|
||||||
|
#changement volontaire ?
|
||||||
|
var best_score : float = best[2] if !best.is_empty() else -1.0
|
||||||
|
var swap : BattleAction = _consider_switch(battle, slot, best_score, usable.is_empty())
|
||||||
|
if swap != null: return swap
|
||||||
|
|
||||||
|
if options.is_empty(): return BattleAction.use_struggle(slot)
|
||||||
|
|
||||||
|
var chosen : Array = options.pick_random() if battle.rng.randf() < randomness else best
|
||||||
|
return BattleAction.use_move(slot, chosen[0], chosen[1])
|
||||||
|
|
||||||
|
func choose_replacement(battle : Battle, slot : BattleSlot) -> PokemonInstance:
|
||||||
|
#remplacement force (pokemon K.O.) : le meilleur rapport de types face aux adversaires
|
||||||
|
var options : Array[PokemonInstance] = battle.get_replacement_options(slot)
|
||||||
|
if options.is_empty(): return null
|
||||||
|
var opponents : Array[BattleSlot] = battle.get_opponents(slot)
|
||||||
|
var best : PokemonInstance = options[0]
|
||||||
|
var best_value : float = -INF
|
||||||
|
for p in options:
|
||||||
|
var value : float = _matchup(_types_of(p), opponents)
|
||||||
|
if value > best_value:
|
||||||
|
best = p
|
||||||
|
best_value = value
|
||||||
|
return best
|
||||||
|
|
||||||
|
#---------- changement volontaire ----------
|
||||||
|
|
||||||
|
func _consider_switch(battle : Battle, slot : BattleSlot, best_attack_score : float, no_usable_move : bool) -> BattleAction:
|
||||||
|
if !can_switch: return null
|
||||||
|
|
||||||
|
var bench : Array[PokemonInstance] = []
|
||||||
|
for p in battle.get_replacement_options(slot):
|
||||||
|
if !_claimed.has(p): bench.append(p)
|
||||||
|
if bench.is_empty(): return null
|
||||||
|
|
||||||
|
var opponents : Array[BattleSlot] = battle.get_opponents(slot)
|
||||||
|
var current : float = _matchup(slot.mon.get_types(), opponents)
|
||||||
|
|
||||||
|
if !no_usable_move:
|
||||||
|
if best_attack_score >= KILL_SCORE: return null #peut achever un adversaire : on attaque
|
||||||
|
var hp_ratio : float = float(slot.mon.pokemon.current_hp) / float(slot.mon.pokemon.get_max_hp())
|
||||||
|
if hp_ratio > hp_threshold or current > matchup_threshold: return null
|
||||||
|
if battle.rng.randf() > switch_chance: return null
|
||||||
|
|
||||||
|
var best : PokemonInstance = null
|
||||||
|
var best_value : float = -INF
|
||||||
|
for p in bench:
|
||||||
|
if !no_usable_move and float(p.current_hp) / float(p.get_max_hp()) < min_replacement_hp: continue
|
||||||
|
var value : float = _matchup(_types_of(p), opponents)
|
||||||
|
if value > best_value:
|
||||||
|
best = p
|
||||||
|
best_value = value
|
||||||
|
if best == null: return null
|
||||||
|
if !no_usable_move and best_value < current + min_improvement: return null
|
||||||
|
|
||||||
|
_claimed.append(best)
|
||||||
|
return BattleAction.switch_pokemon(slot, best)
|
||||||
|
|
||||||
|
func _matchup(my_types : Array[String], opponents : Array[BattleSlot]) -> float:
|
||||||
|
#rapport de types moyen face aux adversaires : (meilleure efficacite offensive) - (meilleure efficacite defensive subie)
|
||||||
|
#positif = avantage, negatif = desavantage
|
||||||
|
if opponents.is_empty(): return 0.0
|
||||||
|
var total : float = 0.0
|
||||||
|
for opponent in opponents:
|
||||||
|
var their_types : Array[String] = opponent.mon.get_types()
|
||||||
|
var offense : float = 0.0
|
||||||
|
for t in my_types:
|
||||||
|
offense = maxf(offense, TypeChart.get_multiplier(t, their_types))
|
||||||
|
var defense : float = 0.0
|
||||||
|
for t in their_types:
|
||||||
|
defense = maxf(defense, TypeChart.get_multiplier(t, my_types))
|
||||||
|
total += offense - defense
|
||||||
|
return total / opponents.size()
|
||||||
|
|
||||||
|
func _types_of(pokemon : PokemonInstance) -> Array[String]:
|
||||||
|
var result : Array[String] = []
|
||||||
|
for t in pokemon.species.types:
|
||||||
|
result.append(TypeChart.type_name(t))
|
||||||
|
return result
|
||||||
|
|
||||||
|
#---------- attaque ----------
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var accuracy : float = 1.0 if move.accuracy <= 0 else move.accuracy / 100.0
|
||||||
|
var spread : bool = targets.size() > 1
|
||||||
|
var score : float = 0.0
|
||||||
|
for t in targets:
|
||||||
|
var damage : int = DamageCalculator.estimate(slot.mon, t.mon, move, spread)
|
||||||
|
var remaining : int = t.mon.pokemon.current_hp
|
||||||
|
var value : float = float(mini(damage, remaining)) * accuracy
|
||||||
|
if t.side == slot.side:
|
||||||
|
score -= value
|
||||||
|
else:
|
||||||
|
score += value
|
||||||
|
if damage >= remaining: score += KILL_SCORE
|
||||||
|
return score
|
||||||
1
game/battle/battle_ai.gd.uid
Normal file
1
game/battle/battle_ai.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dbkfxdanlrke1
|
||||||
11
game/battle/battle_controller.gd
Normal file
11
game/battle/battle_controller.gd
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
class_name BattleController
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#decide pour un participant : IA (BattleAI) ou joueur (menus, a venir).
|
||||||
|
#les deux fonctions peuvent utiliser "await" (le joueur attend un choix dans l'interface).
|
||||||
|
|
||||||
|
func choose_action(_battle : Battle, _slot : BattleSlot) -> BattleAction:
|
||||||
|
return null
|
||||||
|
|
||||||
|
func choose_replacement(_battle : Battle, _slot : BattleSlot) -> PokemonInstance:
|
||||||
|
return null
|
||||||
1
game/battle/battle_controller.gd.uid
Normal file
1
game/battle/battle_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cw4o36nfqrbh8
|
||||||
49
game/battle/battle_debug.gd
Normal file
49
game/battle/battle_debug.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
class_name BattleDebug
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
#banc d'essai : simule un combat complet IA contre IA et ecrit tout dans la console.
|
||||||
|
#ajoute ce node a une scene, regle le format dans l'inspecteur, lance la scene.
|
||||||
|
#utilise des equipes aleatoires (par defaut) : n'abime pas ton equipe de jeu.
|
||||||
|
|
||||||
|
@export var run_on_ready : bool = true
|
||||||
|
@export_range(1, 3) var player_slots : int = 1 #emplacements du joueur
|
||||||
|
@export_range(1, 5) var enemy_count : int = 1 #nombre d'adversaires (chacun est un participant)
|
||||||
|
@export_range(1, 3) var enemy_slots_each : int = 1 #emplacements de chaque adversaire
|
||||||
|
@export_range(1, 6) var team_size : int = 3
|
||||||
|
@export_range(1, 100) var level : int = 10
|
||||||
|
@export var use_game_team : bool = false #true : utilise GameData.player_team (qui sera modifiee !)
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if run_on_ready:
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
run_test()
|
||||||
|
|
||||||
|
func run_test() -> void:
|
||||||
|
var player_team : PokemonTeam = GameData.player_team if use_game_team else _random_team(maxi(team_size, player_slots))
|
||||||
|
|
||||||
|
var setup := BattleSetup.new()
|
||||||
|
setup.add(BattleSetup.player_participant(player_team, player_slots, null, BattleAI.new()))
|
||||||
|
for i in enemy_count:
|
||||||
|
var team : PokemonTeam = _random_team(enemy_slots_each + 1)
|
||||||
|
var participant := BattleParticipant.new("Adversaire %d" % (i + 1), team, 1, BattleAI.new(), enemy_slots_each)
|
||||||
|
setup.add(participant)
|
||||||
|
|
||||||
|
if !setup.is_valid():
|
||||||
|
push_error("BattleDebug : combat invalide (une equipe n'a aucun pokemon apte)")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("===== Combat : %d contre %d =====" % [player_slots, enemy_count * enemy_slots_each])
|
||||||
|
var battle := Battle.new(setup, BattlePrintPresenter.new())
|
||||||
|
var outcome : Battle.Outcome = await battle.run()
|
||||||
|
print("===== Resultat : %s apres %d tours =====" % [Battle.Outcome.keys()[outcome], battle.turn])
|
||||||
|
|
||||||
|
func _random_team(size : int) -> PokemonTeam:
|
||||||
|
var team := PokemonTeam.new()
|
||||||
|
var ids : Array = Pokedex.all_ids()
|
||||||
|
if ids.is_empty():
|
||||||
|
push_error("BattleDebug : le Pokedex ne contient aucun pokemon")
|
||||||
|
return team
|
||||||
|
for i in size:
|
||||||
|
var species : PokemonData = Pokedex.get_pokemon(ids.pick_random())
|
||||||
|
if species: team.add_pokemon(PokemonInstance.create(species, level))
|
||||||
|
return team
|
||||||
1
game/battle/battle_debug.gd.uid
Normal file
1
game/battle/battle_debug.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ulemh0n2lfft
|
||||||
55
game/battle/battle_event.gd
Normal file
55
game/battle/battle_event.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
class_name BattleEvent
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#ce qui se passe pendant un combat. La logique les emet ; la presentation (texte, animations 3D,
|
||||||
|
#barres de PV) les lit un par un. Les emplacements sont identifies par leur id.
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
BATTLE_START, BATTLE_END, TURN_START,
|
||||||
|
SENT_OUT, WITHDRAWN,
|
||||||
|
MOVE_USED, MOVE_MISSED, DAMAGE, CRITICAL,
|
||||||
|
SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT,
|
||||||
|
FAINTED, FLED, MESSAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
var type : Type = Type.MESSAGE
|
||||||
|
var source_slot : int = -1
|
||||||
|
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 hp : int = 0 #PV restants de la cible
|
||||||
|
var max_hp : int = 0
|
||||||
|
var text : String = ""
|
||||||
|
|
||||||
|
static func make(p_type : Type, p_source : BattleSlot = null, p_target : BattleSlot = null) -> BattleEvent:
|
||||||
|
var e := BattleEvent.new()
|
||||||
|
e.type = p_type
|
||||||
|
if p_source != null:
|
||||||
|
e.source_slot = p_source.id
|
||||||
|
if p_source.mon != null: e.source_name = p_source.mon.display_name()
|
||||||
|
if p_target != null:
|
||||||
|
e.target_slot = p_target.id
|
||||||
|
if p_target.mon != null: e.target_name = p_target.mon.display_name()
|
||||||
|
return e
|
||||||
|
|
||||||
|
func describe() -> String:
|
||||||
|
#texte par defaut (boite de dialogue, journal de debug)
|
||||||
|
match type:
|
||||||
|
Type.BATTLE_START: return "Le combat commence !"
|
||||||
|
Type.BATTLE_END: return text
|
||||||
|
Type.TURN_START: return "— Tour %d —" % amount
|
||||||
|
Type.SENT_OUT: return "%s entre en combat !" % source_name
|
||||||
|
Type.WITHDRAWN: return "%s est rappelé !" % source_name
|
||||||
|
Type.MOVE_USED: return "%s utilise %s !" % [source_name, move.get_display_name() if move else "?"]
|
||||||
|
Type.MOVE_MISSED: return "%s évite l'attaque !" % target_name
|
||||||
|
Type.DAMAGE: return "%s perd %d PV (%d/%d)" % [target_name, amount, hp, max_hp]
|
||||||
|
Type.CRITICAL: return "Coup critique !"
|
||||||
|
Type.SUPER_EFFECTIVE: return "C'est super efficace !"
|
||||||
|
Type.NOT_VERY_EFFECTIVE: return "Ce n'est pas très efficace..."
|
||||||
|
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
|
||||||
|
return ""
|
||||||
1
game/battle/battle_event.gd.uid
Normal file
1
game/battle/battle_event.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dxdhsdh0y2fxr
|
||||||
33
game/battle/battle_mon.gd
Normal file
33
game/battle/battle_mon.gd
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
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).
|
||||||
|
|
||||||
|
var pokemon : PokemonInstance
|
||||||
|
var slot_id : int = -1
|
||||||
|
var stages : Dictionary = {} #PokemonNature.Stat -> -6..6 (boosts/malus ; appliques par les effets, V2)
|
||||||
|
|
||||||
|
func _init(p_pokemon : PokemonInstance) -> void:
|
||||||
|
pokemon = p_pokemon
|
||||||
|
|
||||||
|
func display_name() -> String:
|
||||||
|
return pokemon.get_display_name()
|
||||||
|
|
||||||
|
func is_fainted() -> bool:
|
||||||
|
return pokemon.is_fainted()
|
||||||
|
|
||||||
|
func get_types() -> Array[String]:
|
||||||
|
var result : Array[String] = []
|
||||||
|
for t in pokemon.species.types:
|
||||||
|
result.append(TypeChart.type_name(t))
|
||||||
|
return result
|
||||||
|
|
||||||
|
func get_stage(stat : PokemonNature.Stat) -> int:
|
||||||
|
return int(stages.get(stat, 0))
|
||||||
|
|
||||||
|
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
|
||||||
1
game/battle/battle_mon.gd.uid
Normal file
1
game/battle/battle_mon.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://5ja6agfwkfku
|
||||||
36
game/battle/battle_participant.gd
Normal file
36
game/battle/battle_participant.gd
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
class_name BattleParticipant
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#qui prend part au combat : joueur, dresseur ou pokemon sauvage.
|
||||||
|
#il possede une equipe (le "banc"), un controleur, et pilote slot_count emplacements.
|
||||||
|
|
||||||
|
var name : String = ""
|
||||||
|
var team : PokemonTeam
|
||||||
|
var side : int = 0
|
||||||
|
var slot_count : int = 1
|
||||||
|
var controller : BattleController
|
||||||
|
var lead : Array[PokemonInstance] = [] #pokemon a envoyer en premier (ex. celui lance par le joueur)
|
||||||
|
var is_player : bool = false
|
||||||
|
|
||||||
|
func _init(p_name : String, p_team : PokemonTeam, p_side : int, p_controller : BattleController = null, p_slot_count : int = 1) -> void:
|
||||||
|
name = p_name
|
||||||
|
team = p_team
|
||||||
|
side = p_side
|
||||||
|
slot_count = maxi(p_slot_count, 1)
|
||||||
|
controller = p_controller if p_controller != null else BattleAI.new()
|
||||||
|
|
||||||
|
func pick_starters() -> Array[PokemonInstance]:
|
||||||
|
#d'abord les "lead" (aptes), puis l'ordre de l'equipe, jusqu'a slot_count
|
||||||
|
var result : Array[PokemonInstance] = []
|
||||||
|
for p in lead:
|
||||||
|
if result.size() >= slot_count: break
|
||||||
|
if p != null and !p.is_fainted() and team.members.has(p) and !result.has(p):
|
||||||
|
result.append(p)
|
||||||
|
for p in team.members:
|
||||||
|
if result.size() >= slot_count: break
|
||||||
|
if !p.is_fainted() and !result.has(p):
|
||||||
|
result.append(p)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func has_able_pokemon() -> bool:
|
||||||
|
return team.has_able_pokemon()
|
||||||
1
game/battle/battle_participant.gd.uid
Normal file
1
game/battle/battle_participant.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://sxqh5bfin87t
|
||||||
8
game/battle/battle_presenter.gd
Normal file
8
game/battle/battle_presenter.gd
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
class_name BattlePresenter
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#la presentation d'un combat (texte, animations 3D, barres de PV...) herite de cette classe.
|
||||||
|
#present() peut utiliser "await" : le combat attend la fin de l'animation avant l'evenement suivant.
|
||||||
|
|
||||||
|
func present(_event : BattleEvent) -> void:
|
||||||
|
pass
|
||||||
1
game/battle/battle_presenter.gd.uid
Normal file
1
game/battle/battle_presenter.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c1ibvn41p4rxv
|
||||||
8
game/battle/battle_print_presenter.gd
Normal file
8
game/battle/battle_print_presenter.gd
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
class_name BattlePrintPresenter
|
||||||
|
extends BattlePresenter
|
||||||
|
|
||||||
|
#presentation de debug : ecrit les evenements dans la console
|
||||||
|
|
||||||
|
func present(event : BattleEvent) -> void:
|
||||||
|
var line : String = event.describe()
|
||||||
|
if line != "": print(line)
|
||||||
1
game/battle/battle_print_presenter.gd.uid
Normal file
1
game/battle/battle_print_presenter.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ddehrcr54nu2i
|
||||||
38
game/battle/battle_setup.gd
Normal file
38
game/battle/battle_setup.gd
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
class_name BattleSetup
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#description complete d'un combat, construite par le monde avant de le lancer.
|
||||||
|
#le format (1v1, 2v2, 1 contre plusieurs, plusieurs contre 1) vient simplement du nombre
|
||||||
|
#de participants par camp et du nombre d'emplacements (slot_count) de chacun.
|
||||||
|
|
||||||
|
var participants : Array[BattleParticipant] = []
|
||||||
|
var can_flee : bool = true #false pour un combat de dresseur
|
||||||
|
var seed_value : int = 0 #0 = hasard ; autre valeur = combat reproductible (tests)
|
||||||
|
|
||||||
|
func add(participant : BattleParticipant) -> BattleSetup:
|
||||||
|
participants.append(participant)
|
||||||
|
return self
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
var sides : Dictionary = {}
|
||||||
|
for p in participants:
|
||||||
|
if p.team != null and p.team.has_able_pokemon(): sides[p.side] = true
|
||||||
|
return sides.has(0) and sides.has(1)
|
||||||
|
|
||||||
|
#---------- fabriques ----------
|
||||||
|
|
||||||
|
static func player_participant(team : PokemonTeam, slot_count : int = 1, thrown : PokemonInstance = null, controller : BattleController = null) -> BattleParticipant:
|
||||||
|
#joueur qui initie : le pokemon lance passe en premier, puis l'ordre de l'equipe
|
||||||
|
#joueur defie (thrown = null) : l'ordre de l'equipe (le 1er, puis le 2e pour un duo...)
|
||||||
|
var p := BattleParticipant.new("Joueur", team, 0, controller, slot_count)
|
||||||
|
p.is_player = true
|
||||||
|
if thrown != null: p.lead.append(thrown)
|
||||||
|
return p
|
||||||
|
|
||||||
|
static func wild_participant(pokemon : PokemonInstance, controller : BattleController = null) -> BattleParticipant:
|
||||||
|
var team := PokemonTeam.new()
|
||||||
|
team.add_pokemon(pokemon)
|
||||||
|
return BattleParticipant.new(pokemon.get_display_name(), team, 1, controller, 1)
|
||||||
|
|
||||||
|
static func trainer_participant(trainer : TrainerData, slot_count : int = 1, controller : BattleController = null) -> BattleParticipant:
|
||||||
|
return BattleParticipant.new(trainer.get_display_name(), trainer.create_team(), 1, controller, slot_count)
|
||||||
1
game/battle/battle_setup.gd.uid
Normal file
1
game/battle/battle_setup.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://wmyjd47h18dh
|
||||||
25
game/battle/battle_slot.gd
Normal file
25
game/battle/battle_slot.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
class_name BattleSlot
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#un emplacement actif sur le terrain. Contient un BattleMon ou reste vide.
|
||||||
|
|
||||||
|
var id : int = 0 #unique dans le combat
|
||||||
|
var side : int = 0 #0 = camp du joueur, 1 = camp adverse
|
||||||
|
var index : int = 0 #position dans son camp (0, 1, 2...)
|
||||||
|
var participant : BattleParticipant
|
||||||
|
var mon : BattleMon = null
|
||||||
|
|
||||||
|
func is_occupied() -> bool:
|
||||||
|
return mon != null
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
#occupe par un pokemon encore apte au combat
|
||||||
|
return mon != null and !mon.is_fainted()
|
||||||
|
|
||||||
|
func set_pokemon(pokemon : PokemonInstance) -> BattleMon:
|
||||||
|
mon = BattleMon.new(pokemon)
|
||||||
|
mon.slot_id = id
|
||||||
|
return mon
|
||||||
|
|
||||||
|
func clear() -> void:
|
||||||
|
mon = null
|
||||||
1
game/battle/battle_slot.gd.uid
Normal file
1
game/battle/battle_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://daogeyd6po2en
|
||||||
40
game/battle/damage_calculator.gd
Normal file
40
game/battle/damage_calculator.gd
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
class_name DamageCalculator
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CRIT_CHANCE : float = 1.0 / 24.0
|
||||||
|
const CRIT_MULTIPLIER : float = 1.5
|
||||||
|
const STAB_MULTIPLIER : float = 1.5
|
||||||
|
const SPREAD_MULTIPLIER : float = 0.75 #capacite touchant plusieurs cibles
|
||||||
|
|
||||||
|
static func calculate(attacker : BattleMon, defender : BattleMon, move : MoveData, rng : RandomNumberGenerator = null, spread : bool = false, deterministic : bool = false) -> Dictionary:
|
||||||
|
#renvoie {"damage": int, "effectiveness": float, "critical": bool}
|
||||||
|
var result : Dictionary = {"damage": 0, "effectiveness": 1.0, "critical": false}
|
||||||
|
if move.power <= 0 or move.category == "status": return result
|
||||||
|
|
||||||
|
var effectiveness : float = TypeChart.get_multiplier(move.type, defender.get_types())
|
||||||
|
result["effectiveness"] = effectiveness
|
||||||
|
if effectiveness == 0.0: return result
|
||||||
|
|
||||||
|
var physical : bool = move.category == "physical"
|
||||||
|
var attack : float = attacker.get_stat(PokemonNature.Stat.ATTACK if physical else PokemonNature.Stat.SP_ATTACK)
|
||||||
|
var defense : float = defender.get_stat(PokemonNature.Stat.DEFENSE if physical else PokemonNature.Stat.SP_DEFENSE)
|
||||||
|
var level : int = attacker.pokemon.level
|
||||||
|
|
||||||
|
#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 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
|
||||||
|
|
||||||
|
damage *= roll * stab * effectiveness
|
||||||
|
if critical: damage *= CRIT_MULTIPLIER
|
||||||
|
if spread: damage *= SPREAD_MULTIPLIER
|
||||||
|
|
||||||
|
result["critical"] = critical
|
||||||
|
result["damage"] = maxi(1, floori(damage))
|
||||||
|
return result
|
||||||
|
|
||||||
|
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"])
|
||||||
1
game/battle/damage_calculator.gd.uid
Normal file
1
game/battle/damage_calculator.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d2o31jkem2b5l
|
||||||
67
game/battle/target_resolver.gd
Normal file
67
game/battle/target_resolver.gd
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
class_name TargetResolver
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#convertit MoveData.target (valeurs PokeAPI) + le choix du joueur en liste d'emplacements.
|
||||||
|
|
||||||
|
const SELECTABLE : Array[String] = ["selected-pokemon", "selected-pokemon-me-first", "specific-move", "ally", "user-or-ally"]
|
||||||
|
|
||||||
|
static func get_target_type(move : MoveData) -> String:
|
||||||
|
return move.target if move.target != "" else "selected-pokemon"
|
||||||
|
|
||||||
|
static func get_selectable_targets(battle : Battle, actor : BattleSlot, move : MoveData) -> Array[BattleSlot]:
|
||||||
|
#emplacements parmi lesquels le joueur peut choisir (ordre : par camp puis par position)
|
||||||
|
var result : Array[BattleSlot] = []
|
||||||
|
match get_target_type(move):
|
||||||
|
"ally":
|
||||||
|
result = battle.get_allies(actor)
|
||||||
|
"user-or-ally":
|
||||||
|
result = battle.get_allies(actor)
|
||||||
|
result.append(actor)
|
||||||
|
_:
|
||||||
|
for s in battle.get_active_slots():
|
||||||
|
if s != actor: result.append(s)
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func needs_selection(battle : Battle, actor : BattleSlot, move : MoveData) -> bool:
|
||||||
|
#une etape "choisir la cible" n'est utile que s'il y a vraiment un choix
|
||||||
|
return SELECTABLE.has(get_target_type(move)) and get_selectable_targets(battle, actor, move).size() > 1
|
||||||
|
|
||||||
|
static func resolve(battle : Battle, actor : BattleSlot, move : MoveData, chosen : BattleSlot) -> Array[BattleSlot]:
|
||||||
|
#liste finale des cibles au moment ou la capacite est utilisee (cible K.O. entre-temps : redirigee)
|
||||||
|
var opponents : Array[BattleSlot] = battle.get_opponents(actor)
|
||||||
|
var allies : Array[BattleSlot] = battle.get_allies(actor)
|
||||||
|
var result : Array[BattleSlot] = []
|
||||||
|
|
||||||
|
match get_target_type(move):
|
||||||
|
"user":
|
||||||
|
result.append(actor)
|
||||||
|
"all-opponents":
|
||||||
|
result = opponents
|
||||||
|
"random-opponent":
|
||||||
|
if !opponents.is_empty(): result.append(opponents.pick_random())
|
||||||
|
"all-other-pokemon":
|
||||||
|
result.append_array(opponents)
|
||||||
|
result.append_array(allies)
|
||||||
|
"all-pokemon", "entire-field":
|
||||||
|
result.append_array(opponents)
|
||||||
|
result.append_array(allies)
|
||||||
|
result.append(actor)
|
||||||
|
"all-allies", "user-and-allies":
|
||||||
|
result = allies
|
||||||
|
result.append(actor)
|
||||||
|
"ally", "user-or-ally":
|
||||||
|
if chosen != null and chosen.is_active() and (allies.has(chosen) or chosen == actor):
|
||||||
|
result.append(chosen)
|
||||||
|
elif !allies.is_empty():
|
||||||
|
result.append(allies.pick_random())
|
||||||
|
elif get_target_type(move) == "user-or-ally":
|
||||||
|
result.append(actor)
|
||||||
|
"users-field", "opponents-field", "fainting-pokemon":
|
||||||
|
pass #effets de terrain : plus tard
|
||||||
|
_:
|
||||||
|
#cible choisie ; si elle est K.O. ou absente, redirection vers un adversaire au hasard
|
||||||
|
if chosen != null and chosen != actor and chosen.is_active():
|
||||||
|
result.append(chosen)
|
||||||
|
elif !opponents.is_empty():
|
||||||
|
result.append(opponents.pick_random())
|
||||||
|
return result
|
||||||
1
game/battle/target_resolver.gd.uid
Normal file
1
game/battle/target_resolver.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bum2it3yoslcj
|
||||||
41
game/battle/type_chart.gd
Normal file
41
game/battle/type_chart.gd
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
class_name TypeChart
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
#table d'efficacite des types (generation 6+, avec Fee). Seules les valeurs differentes de 1 sont listees.
|
||||||
|
#point de remplacement unique si tu veux la deriver plus tard des TypeData importes.
|
||||||
|
|
||||||
|
const CHART : Dictionary = {
|
||||||
|
"normal": {"rock": 0.5, "steel": 0.5, "ghost": 0.0},
|
||||||
|
"fire": {"fire": 0.5, "water": 0.5, "grass": 2.0, "ice": 2.0, "bug": 2.0, "rock": 0.5, "dragon": 0.5, "steel": 2.0},
|
||||||
|
"water": {"fire": 2.0, "water": 0.5, "grass": 0.5, "ground": 2.0, "rock": 2.0, "dragon": 0.5},
|
||||||
|
"electric": {"water": 2.0, "electric": 0.5, "grass": 0.5, "ground": 0.0, "flying": 2.0, "dragon": 0.5},
|
||||||
|
"grass": {"fire": 0.5, "water": 2.0, "grass": 0.5, "poison": 0.5, "ground": 2.0, "flying": 0.5, "bug": 0.5, "rock": 2.0, "dragon": 0.5, "steel": 0.5},
|
||||||
|
"ice": {"fire": 0.5, "water": 0.5, "grass": 2.0, "ice": 0.5, "ground": 2.0, "flying": 2.0, "dragon": 2.0, "steel": 0.5},
|
||||||
|
"fighting": {"normal": 2.0, "ice": 2.0, "poison": 0.5, "flying": 0.5, "psychic": 0.5, "bug": 0.5, "rock": 2.0, "ghost": 0.0, "dark": 2.0, "steel": 2.0, "fairy": 0.5},
|
||||||
|
"poison": {"grass": 2.0, "poison": 0.5, "ground": 0.5, "rock": 0.5, "ghost": 0.5, "steel": 0.0, "fairy": 2.0},
|
||||||
|
"ground": {"fire": 2.0, "electric": 2.0, "grass": 0.5, "poison": 2.0, "flying": 0.0, "bug": 0.5, "rock": 2.0, "steel": 2.0},
|
||||||
|
"flying": {"electric": 0.5, "grass": 2.0, "fighting": 2.0, "bug": 2.0, "rock": 0.5, "steel": 0.5},
|
||||||
|
"psychic": {"fighting": 2.0, "poison": 2.0, "psychic": 0.5, "dark": 0.0, "steel": 0.5},
|
||||||
|
"bug": {"fire": 0.5, "grass": 2.0, "fighting": 0.5, "poison": 0.5, "flying": 0.5, "psychic": 2.0, "ghost": 0.5, "dark": 2.0, "steel": 0.5, "fairy": 0.5},
|
||||||
|
"rock": {"fire": 2.0, "ice": 2.0, "fighting": 0.5, "ground": 0.5, "flying": 2.0, "bug": 2.0, "steel": 0.5},
|
||||||
|
"ghost": {"normal": 0.0, "psychic": 2.0, "ghost": 2.0, "dark": 0.5},
|
||||||
|
"dragon": {"dragon": 2.0, "steel": 0.5, "fairy": 0.0},
|
||||||
|
"dark": {"fighting": 0.5, "psychic": 2.0, "ghost": 2.0, "dark": 0.5, "fairy": 0.5},
|
||||||
|
"steel": {"fire": 0.5, "water": 0.5, "electric": 0.5, "ice": 2.0, "rock": 2.0, "steel": 0.5, "fairy": 2.0},
|
||||||
|
"fairy": {"fire": 0.5, "fighting": 2.0, "poison": 0.5, "dragon": 2.0, "dark": 2.0, "steel": 0.5},
|
||||||
|
}
|
||||||
|
|
||||||
|
static func get_multiplier(attack_type : String, defender_types : Array[String]) -> float:
|
||||||
|
var row : Dictionary = CHART.get(attack_type.to_lower(), {})
|
||||||
|
var multiplier : float = 1.0
|
||||||
|
for t in defender_types:
|
||||||
|
multiplier *= float(row.get(t, 1.0))
|
||||||
|
return multiplier
|
||||||
|
|
||||||
|
static func type_name(type_data : Resource) -> String:
|
||||||
|
#nom d'API du type ("electric"...) ; A ADAPTER si TypeData utilise un autre champ.
|
||||||
|
#a defaut, on utilise le nom du fichier (ton Pokedex charge les types par "<nom>.tres")
|
||||||
|
var n : Variant = type_data.get("api_name")
|
||||||
|
if n == null or str(n) == "":
|
||||||
|
n = type_data.resource_path.get_file().get_basename()
|
||||||
|
return str(n).to_lower()
|
||||||
1
game/battle/type_chart.gd.uid
Normal file
1
game/battle/type_chart.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bmlmqh38u8hpu
|
||||||
6
game/world/battle.tscn
Normal file
6
game/world/battle.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene format=3 uid="uid://cu5ednry26cds"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ulemh0n2lfft" path="res://game/battle/battle_debug.gd" id="1_5bjgp"]
|
||||||
|
|
||||||
|
[node name="Battle" type="Node" unique_id=520840168]
|
||||||
|
script = ExtResource("1_5bjgp")
|
||||||
Loading…
Reference in a new issue