Tales-of-PKMN/game/battle/battle_world_presenter.gd

199 lines
7.4 KiB
GDScript

class_name BattleWorldPresenter
extends BattlePresenter
#met le combat en scene dans le monde 3D :
#- place les pokemon (un BattleActor par emplacement) face a face, a l'endroit de la rencontre
#- deplace une camera de combat qui cadre l'ensemble
#- joue les animations et affiche les messages / PV via BattleUI
#
#le monde (hors combat) est en pause : tout ce qui est ici fonctionne en mode "toujours".
var ui : BattleUI
var battle : Battle
#formation
var side_distance : float = 7.0 #distance entre les deux camps
var lateral_spacing : float = 2.6 #ecart entre deux pokemon d'un meme camp
var ground_mask : int = 1
#camera
var camera_back : float = 4.5 #recul derriere le camp du joueur
var camera_height : float = 3.4
var camera_move_time : float = 0.9
var _actors : Dictionary = {} #id d'emplacement -> BattleActor
var _formation : Dictionary = {} #id d'emplacement -> position au sol
var _center : Vector3 = Vector3.ZERO
var _axis : Vector3 = Vector3.FORWARD #direction camp joueur -> camp adverse
var _camera : Camera3D = null
var _previous_camera : Camera3D = null
var _world_root : Node = null #ou ajouter camera et acteurs : le monde que le joueur voit
func _init(p_ui : BattleUI) -> void:
ui = p_ui
ui.highlight_requested.connect(_on_highlight_requested)
#---------- mise en place ----------
func setup(p_battle : Battle, player_pos : Vector3, enemy_pos : Vector3, p_world_root : Node, p_game_camera : Camera3D) -> void:
battle = p_battle
_world_root = p_world_root
_previous_camera = p_game_camera
_axis = enemy_pos - player_pos
_axis.y = 0.0
if _axis.length() < 0.1:
#pas de direction evidente : on utilise la direction de la camera du jeu
var cam : Camera3D = _previous_camera
_axis = -cam.global_basis.z if cam else Vector3.FORWARD
_axis.y = 0.0
_axis = _axis.normalized()
_center = (player_pos + enemy_pos) * 0.5
var lateral : Vector3 = _axis.cross(Vector3.UP).normalized()
var count_per_side : Dictionary = {0: 0, 1: 0}
for slot in battle.slots:
count_per_side[slot.side] += 1
for slot in battle.slots:
var n : int = count_per_side[slot.side]
var offset : float = (slot.index - (n - 1) * 0.5) * lateral_spacing
var side_sign : float = -1.0 if slot.side == 0 else 1.0
var pos : Vector3 = _center + _axis * side_sign * side_distance * 0.5 + lateral * offset
_formation[slot.id] = _snap_to_ground(pos)
_start_camera(maxi(count_per_side[0], count_per_side[1]))
func _start_camera(max_slots : int) -> void:
var back : float = camera_back + max_slots * 0.8
var cam_pos : Vector3 = _center - _axis * (side_distance * 0.5 + back) + Vector3.UP * (camera_height + max_slots * 0.3)
var look_target : Vector3 = _center + Vector3.UP * 1.0
var final_transform := Transform3D(Basis.looking_at(look_target - cam_pos, Vector3.UP), cam_pos)
_camera = Camera3D.new()
_world_root.add_child(_camera)
_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)
func cleanup() -> void:
#fin du combat : ecran propre, camera du jeu reprise
ui.highlight_requested.emit([])
ui.hide_message()
ui.clear_panels()
for actor in _actors.values():
if is_instance_valid(actor): (actor as Node).queue_free()
_actors.clear()
if _camera != null:
if _previous_camera != null and is_instance_valid(_previous_camera):
var tween : Tween = ui.create_tween()
tween.tween_property(_camera, "global_transform", _previous_camera.global_transform, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
await tween.finished
_previous_camera.make_current()
_camera.queue_free()
_camera = null
#---------- evenements ----------
func present(event : BattleEvent) -> void:
match event.type:
BattleEvent.Type.BATTLE_START:
await ui.show_message(event.describe(), 1.0)
BattleEvent.Type.SENT_OUT:
await _show_actor(event.source_slot)
await ui.show_message(event.describe(), 0.9)
BattleEvent.Type.WITHDRAWN:
await ui.show_message(event.describe(), 0.7)
await _remove_actor(event.source_slot)
BattleEvent.Type.MOVE_USED:
var actor : BattleActor = _actors.get(event.source_slot)
if actor: actor.play_attack(_opponents_center(event.source_slot)) #en parallele du message
await ui.show_message(event.describe(), 1.0)
BattleEvent.Type.DAMAGE:
var actor : BattleActor = _actors.get(event.target_slot)
if actor: actor.play_hit()
await ui.animate_hp(event.target_slot, event.hp, event.max_hp)
BattleEvent.Type.FAINTED:
var actor : BattleActor = _actors.get(event.source_slot)
if actor: actor.play_faint()
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
_:
#MOVE_MISSED, CRITICAL, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, NO_EFFECT, FLED, MESSAGE, BATTLE_END...
var text : String = event.describe()
if text != "": await ui.show_message(text, 1.0)
func _show_actor(slot_id : int) -> void:
var slot : BattleSlot = battle.get_slot(slot_id)
if slot == null or slot.mon == null: return
if _actors.has(slot_id):
(_actors[slot_id] as Node).queue_free()
_actors.erase(slot_id)
var actor := BattleActor.new()
_world_root.add_child(actor)
actor.setup(slot.mon.pokemon, slot_id)
var facing : Vector3 = _center + _axis * (side_distance if slot.side == 0 else -side_distance)
actor.place(_formation.get(slot_id, _center), facing)
_actors[slot_id] = actor
ui.set_slot_pokemon(slot)
await actor.play_enter()
func _remove_actor(slot_id : int) -> void:
ui.remove_slot_panel(slot_id)
var actor : BattleActor = _actors.get(slot_id)
if actor == null: return
await actor.play_leave()
actor.queue_free()
_actors.erase(slot_id)
func _on_highlight_requested(slot_ids : Array) -> void:
for id in _actors:
(_actors[id] as BattleActor).set_highlight(slot_ids.has(id))
#---------- utilitaires ----------
func _opponents_center(slot_id : int) -> Vector3:
var slot : BattleSlot = battle.get_slot(slot_id)
if slot == null: return _center
var total : Vector3 = Vector3.ZERO
var count : int = 0
for other in battle.slots:
if other.side != slot.side and _actors.has(other.id):
total += (_actors[other.id] as BattleActor).home
count += 1
return total / count if count > 0 else _center
func _snap_to_ground(point : Vector3) -> Vector3:
var world : World3D = _previous_camera.get_world_3d() if _previous_camera != null else _world_root.get_viewport().world_3d
var space : PhysicsDirectSpaceState3D = world.direct_space_state
var query := PhysicsRayQueryParameters3D.create(point + Vector3.UP * 3.0, point + Vector3.DOWN * 8.0, ground_mask)
var hit : Dictionary = space.intersect_ray(query)
return hit["position"] if !hit.is_empty() else point