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