init
This commit is contained in:
46
scripts/camera_controller.gd
Normal file
46
scripts/camera_controller.gd
Normal file
@@ -0,0 +1,46 @@
|
||||
extends Node3D
|
||||
class_name CameraController
|
||||
|
||||
# Контроллер камеры в стиле Path of Exile 2
|
||||
# Изометрическая камера, которая следует за игроком
|
||||
|
||||
@export var target: Node3D = null
|
||||
@export var camera_distance: float = 20.0
|
||||
@export var camera_height: float = 15.0
|
||||
@export var camera_angle: float = 45.0 # Угол наклона камеры
|
||||
@export var follow_speed: float = 5.0
|
||||
|
||||
var camera: Camera3D
|
||||
|
||||
func _ready():
|
||||
camera = get_node_or_null("Camera3D")
|
||||
if not camera:
|
||||
camera = Camera3D.new()
|
||||
add_child(camera)
|
||||
|
||||
# Находим игрока если цель не установлена
|
||||
if not target:
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
if players.size() > 0:
|
||||
target = players[0]
|
||||
|
||||
func _process(delta):
|
||||
if not target or not is_instance_valid(target):
|
||||
return
|
||||
|
||||
# Вычисляем позицию камеры
|
||||
var target_pos = target.global_position
|
||||
var angle_rad = deg_to_rad(camera_angle)
|
||||
|
||||
# Изометрическая позиция камеры (диагональный вид сверху, как в POE 2)
|
||||
# Камера находится под углом 45 градусов по диагонали
|
||||
var offset_x = camera_distance * 0.707 # cos(45) = 0.707
|
||||
var offset_z = camera_distance * 0.707 # sin(45) = 0.707
|
||||
var offset_y = camera_height
|
||||
|
||||
# Плавное следование за игроком
|
||||
var desired_pos = target_pos + Vector3(offset_x, offset_y, offset_z)
|
||||
global_position = global_position.lerp(desired_pos, follow_speed * delta)
|
||||
|
||||
# Камера всегда смотрит на игрока
|
||||
camera.look_at(target_pos, Vector3.UP)
|
||||
1
scripts/camera_controller.gd.uid
Normal file
1
scripts/camera_controller.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://28onm6d36qbo
|
||||
77
scripts/enemy.gd
Normal file
77
scripts/enemy.gd
Normal file
@@ -0,0 +1,77 @@
|
||||
extends Unit
|
||||
class_name Enemy
|
||||
|
||||
# Класс противника
|
||||
|
||||
@export var detection_range: float = 10.0
|
||||
@export var attack_range: float = 2.0
|
||||
@export var attack_damage: float = 10.0
|
||||
@export var attack_cooldown: float = 1.0
|
||||
|
||||
var target: Node3D = null
|
||||
var attack_timer: float = 0.0
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
|
||||
func _physics_process(delta):
|
||||
attack_timer -= delta
|
||||
|
||||
# Поиск цели (игрока)
|
||||
if target == null or not is_instance_valid(target):
|
||||
find_target()
|
||||
|
||||
# Движение к цели
|
||||
if target and is_instance_valid(target):
|
||||
var distance = global_position.distance_to(target.global_position)
|
||||
|
||||
if distance <= attack_range:
|
||||
# Атака - поворачиваемся к цели
|
||||
var look_direction = (target.global_position - global_position)
|
||||
look_direction.y = 0
|
||||
if look_direction.length() > 0.1:
|
||||
var angle = atan2(look_direction.x, look_direction.z)
|
||||
rotation.y = angle
|
||||
|
||||
if attack_timer <= 0:
|
||||
attack(target)
|
||||
attack_timer = attack_cooldown
|
||||
elif distance <= detection_range:
|
||||
# Преследование - поворачиваемся и бежим к цели
|
||||
var direction = (target.global_position - global_position).normalized()
|
||||
velocity.x = direction.x * speed
|
||||
velocity.z = direction.z * speed
|
||||
|
||||
# Поворачиваем врага в сторону цели
|
||||
var look_direction = (target.global_position - global_position)
|
||||
look_direction.y = 0
|
||||
if look_direction.length() > 0.1:
|
||||
var angle = atan2(look_direction.x, look_direction.z)
|
||||
rotation.y = angle
|
||||
else:
|
||||
# Остановка - не поворачиваемся
|
||||
velocity.x = move_toward(velocity.x, 0, speed)
|
||||
velocity.z = move_toward(velocity.z, 0, speed)
|
||||
else:
|
||||
# Остановка если нет цели
|
||||
velocity.x = move_toward(velocity.x, 0, speed)
|
||||
velocity.z = move_toward(velocity.z, 0, speed)
|
||||
|
||||
# Применяем гравитацию
|
||||
if not is_on_floor():
|
||||
velocity.y -= 9.8 * delta
|
||||
else:
|
||||
velocity.y = 0
|
||||
|
||||
super._physics_process(delta)
|
||||
|
||||
func find_target():
|
||||
# Ищем игрока в сцене
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
if players.size() > 0:
|
||||
target = players[0]
|
||||
|
||||
func attack(target_unit: Node3D):
|
||||
# Атака цели
|
||||
if target_unit.has_method("take_damage"):
|
||||
target_unit.take_damage(attack_damage)
|
||||
1
scripts/enemy.gd.uid
Normal file
1
scripts/enemy.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://8vuywuv7ebsl
|
||||
87
scripts/enemy_health_bar.gd
Normal file
87
scripts/enemy_health_bar.gd
Normal file
@@ -0,0 +1,87 @@
|
||||
extends Node3D
|
||||
class_name EnemyHealthBar
|
||||
|
||||
# Скрипт для отображения полоски здоровья над врагом
|
||||
|
||||
@onready var background_bar: MeshInstance3D = $BackgroundBar
|
||||
@onready var health_bar: MeshInstance3D = $HealthBar
|
||||
|
||||
var unit: Unit = null
|
||||
var bar_width: float = 3.0
|
||||
var bar_height: float = 0.2
|
||||
|
||||
func _ready():
|
||||
# Находим родительский Unit (враг)
|
||||
unit = get_parent() as Unit
|
||||
if unit:
|
||||
# Подключаемся к сигналу изменения здоровья
|
||||
unit.health_changed.connect(_on_health_changed)
|
||||
# Изначально скрываем полоску (если здоровье 100%)
|
||||
update_visibility()
|
||||
update_health_bar()
|
||||
else:
|
||||
print("EnemyHealthBar: Не найден Unit!")
|
||||
|
||||
# Убеждаемся, что узлы найдены
|
||||
if not background_bar:
|
||||
print("EnemyHealthBar: Не найден BackgroundBar!")
|
||||
if not health_bar:
|
||||
print("EnemyHealthBar: Не найден HealthBar!")
|
||||
|
||||
func _process(_delta):
|
||||
# Поворачиваем полоску к камере
|
||||
var camera = get_viewport().get_camera_3d()
|
||||
if camera and visible:
|
||||
# BoxMesh по умолчанию ориентирован правильно
|
||||
# Поворачиваем полоску так, чтобы она была обращена к камере
|
||||
# Используем только горизонтальный поворот (по оси Y), чтобы полоска оставалась горизонтальной
|
||||
var direction_to_camera = (camera.global_position - global_position).normalized()
|
||||
if direction_to_camera.length() > 0.01:
|
||||
var horizontal_direction = direction_to_camera
|
||||
horizontal_direction.y = 0
|
||||
horizontal_direction = horizontal_direction.normalized()
|
||||
if horizontal_direction.length() > 0.01:
|
||||
# Поворачиваем только по оси Y
|
||||
var angle = atan2(horizontal_direction.x, horizontal_direction.z)
|
||||
rotation.y = angle
|
||||
|
||||
# Обновляем полоску после поворота (чтобы позиция была правильной)
|
||||
if unit:
|
||||
update_health_bar()
|
||||
|
||||
func _on_health_changed(new_health: float):
|
||||
update_health_bar()
|
||||
update_visibility()
|
||||
|
||||
func update_health_bar():
|
||||
if not unit or not health_bar:
|
||||
return
|
||||
|
||||
var health_percent = clamp(unit.health / unit.max_health, 0.0, 1.0)
|
||||
|
||||
# Масштабируем полоску здоровья в зависимости от процента
|
||||
# Убеждаемся, что масштаб всегда положительный и достаточно большой для видимости
|
||||
var scale_x = max(health_percent, 0.05) # Минимум 0.05 для лучшей видимости
|
||||
health_bar.scale.x = scale_x
|
||||
|
||||
# Смещаем полоску влево, чтобы она уменьшалась справа налево
|
||||
# Используем локальные координаты относительно центра
|
||||
# Центр полоски должен быть в центре, поэтому смещаем на половину разницы
|
||||
var offset = -(bar_width * (1.0 - health_percent)) / 2.0
|
||||
health_bar.position.x = offset
|
||||
# Убеждаемся, что зеленая полоска немного впереди красной для правильного отображения
|
||||
health_bar.position.z = 0.01
|
||||
|
||||
func update_visibility():
|
||||
if not unit:
|
||||
visible = false
|
||||
return
|
||||
|
||||
# Показываем полоску только если здоровье меньше 100%
|
||||
var should_show = unit.health < unit.max_health
|
||||
visible = should_show
|
||||
if background_bar:
|
||||
background_bar.visible = should_show
|
||||
if health_bar:
|
||||
health_bar.visible = should_show
|
||||
|
||||
1
scripts/enemy_health_bar.gd.uid
Normal file
1
scripts/enemy_health_bar.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c7bnw4xa03juj
|
||||
53
scripts/hud.gd
Normal file
53
scripts/hud.gd
Normal file
@@ -0,0 +1,53 @@
|
||||
extends Control
|
||||
class_name HUD
|
||||
|
||||
# HUD для отображения информации об игроке
|
||||
|
||||
@onready var health_bar: ProgressBar = $HealthBar
|
||||
@onready var health_label: Label = $HealthBar/HealthLabel
|
||||
@onready var dash_cooldown_bar: ProgressBar = $DashCooldownBar
|
||||
@onready var dash_cooldown_label: Label = $DashCooldownBar/DashCooldownLabel
|
||||
|
||||
var player: Player = null
|
||||
|
||||
func _ready():
|
||||
# Находим игрока в сцене
|
||||
call_deferred("find_player")
|
||||
|
||||
func find_player():
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
if players.size() > 0:
|
||||
player = players[0] as Player
|
||||
if player:
|
||||
# Подключаемся к сигналам
|
||||
player.health_changed.connect(_on_player_health_changed)
|
||||
player.dash_cooldown_changed.connect(_on_dash_cooldown_changed)
|
||||
# Обновляем отображение
|
||||
update_health_display()
|
||||
update_dash_cooldown_display()
|
||||
|
||||
func _on_player_health_changed(new_health: float):
|
||||
update_health_display()
|
||||
|
||||
func _on_dash_cooldown_changed(remaining_time: float):
|
||||
update_dash_cooldown_display()
|
||||
|
||||
func update_health_display():
|
||||
if not player:
|
||||
return
|
||||
|
||||
var health_percent = (player.health / player.max_health) * 100.0
|
||||
health_bar.value = health_percent
|
||||
health_label.text = "HP: %d / %d" % [int(player.health), int(player.max_health)]
|
||||
|
||||
func update_dash_cooldown_display():
|
||||
if not player:
|
||||
return
|
||||
|
||||
var cooldown_percent = (player.dash_cooldown_timer / player.dash_cooldown) * 100.0
|
||||
dash_cooldown_bar.value = cooldown_percent
|
||||
|
||||
if player.dash_cooldown_timer > 0.0:
|
||||
dash_cooldown_label.text = "Рывок: %.1f" % player.dash_cooldown_timer
|
||||
else:
|
||||
dash_cooldown_label.text = "Рывок: готов (Пробел)"
|
||||
1
scripts/hud.gd.uid
Normal file
1
scripts/hud.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://buc8nflhbd2kx
|
||||
29
scripts/main_menu.gd
Normal file
29
scripts/main_menu.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
extends Control
|
||||
|
||||
# Скрипт главного меню
|
||||
|
||||
func _ready():
|
||||
# Подключаем сигналы кнопок
|
||||
var start_button = get_node_or_null("VBoxContainer/StartButton")
|
||||
var settings_button = get_node_or_null("VBoxContainer/SettingsButton")
|
||||
var quit_button = get_node_or_null("VBoxContainer/QuitButton")
|
||||
|
||||
if start_button:
|
||||
start_button.pressed.connect(_on_start_button_pressed)
|
||||
if settings_button:
|
||||
settings_button.pressed.connect(_on_settings_button_pressed)
|
||||
if quit_button:
|
||||
quit_button.pressed.connect(_on_quit_button_pressed)
|
||||
|
||||
func _on_start_button_pressed():
|
||||
# Переход к игре
|
||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||
|
||||
func _on_settings_button_pressed():
|
||||
# TODO: Открыть меню настроек
|
||||
print("Настройки (пока не реализовано)")
|
||||
|
||||
func _on_quit_button_pressed():
|
||||
# Выход из игры
|
||||
get_tree().quit()
|
||||
|
||||
1
scripts/main_menu.gd.uid
Normal file
1
scripts/main_menu.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dpfj2k2b4353w
|
||||
124
scripts/parabolic_projectile.gd
Normal file
124
scripts/parabolic_projectile.gd
Normal file
@@ -0,0 +1,124 @@
|
||||
extends Projectile
|
||||
class_name ParabolicProjectile
|
||||
|
||||
# Параболический снаряд для броска
|
||||
|
||||
@export var projectile_gravity: float = 200
|
||||
@export var arc_height: float = 15.0 # Увеличена высота параболы
|
||||
|
||||
var target_position: Vector3 = Vector3.ZERO
|
||||
var start_position: Vector3 = Vector3.ZERO
|
||||
var travel_time: float = 0.0
|
||||
var total_time: float = 0.0
|
||||
|
||||
func _ready():
|
||||
start_position = global_position
|
||||
|
||||
# Проверяем, что target_position установлен
|
||||
if target_position == Vector3.ZERO:
|
||||
# Если не установлен, используем направление вперед
|
||||
target_position = start_position + Vector3(0, 0, -10.0)
|
||||
target_position.y = start_position.y
|
||||
|
||||
# Вычисляем горизонтальное расстояние
|
||||
var horizontal_distance = Vector3(start_position.x, 0, start_position.z).distance_to(Vector3(target_position.x, 0, target_position.z))
|
||||
|
||||
# Вычисляем правильную траекторию для попадания в цель
|
||||
var horizontal_dir = (target_position - start_position)
|
||||
horizontal_dir.y = 0
|
||||
|
||||
# Если цель слишком близко (включая клик на самого игрока), устанавливаем минимальное расстояние и направление
|
||||
if horizontal_distance < 1.0:
|
||||
# Если направление нулевое или очень маленькое, используем направление вперед от игрока
|
||||
if horizontal_dir.length() < 0.1:
|
||||
# Используем направление камеры или направление вперед
|
||||
var camera = get_viewport().get_camera_3d()
|
||||
if camera:
|
||||
var forward = -camera.global_transform.basis.z
|
||||
forward.y = 0
|
||||
horizontal_dir = forward.normalized()
|
||||
else:
|
||||
horizontal_dir = Vector3(0, 0, -1) # Направление по умолчанию
|
||||
else:
|
||||
horizontal_dir = horizontal_dir.normalized()
|
||||
|
||||
# Устанавливаем минимальное расстояние
|
||||
horizontal_distance = 1.0
|
||||
# Обновляем target_position на минимальном расстоянии от стартовой позиции
|
||||
target_position = start_position + horizontal_dir * horizontal_distance
|
||||
target_position.y = 0.0
|
||||
else:
|
||||
horizontal_dir = horizontal_dir.normalized()
|
||||
|
||||
# Время подъема до максимальной высоты
|
||||
var time_to_peak = sqrt(2 * arc_height / projectile_gravity)
|
||||
# Время полета вниз (примерно такое же)
|
||||
var time_to_fall = time_to_peak
|
||||
# Общее время полета по вертикали
|
||||
var vertical_time = time_to_peak + time_to_fall
|
||||
|
||||
# Горизонтальная скорость должна обеспечить попадание в цель за время полета
|
||||
var horizontal_speed = horizontal_distance / vertical_time
|
||||
|
||||
# Начальная вертикальная скорость для достижения нужной высоты
|
||||
var vertical_speed = sqrt(2 * projectile_gravity * arc_height)
|
||||
|
||||
# Общее время полета
|
||||
total_time = vertical_time
|
||||
|
||||
# Устанавливаем начальную скорость
|
||||
velocity = horizontal_dir * horizontal_speed + Vector3.UP * vertical_speed
|
||||
|
||||
# Вызываем super._ready() после установки velocity
|
||||
super._ready()
|
||||
|
||||
# Отключаем мониторинг коллизий при полете
|
||||
monitoring = false
|
||||
|
||||
func _physics_process(delta):
|
||||
if has_hit:
|
||||
return
|
||||
|
||||
# Если velocity не установлена, не двигаемся
|
||||
if velocity == Vector3.ZERO:
|
||||
return
|
||||
|
||||
# Проверяем время жизни
|
||||
lifetime_timer -= delta
|
||||
if lifetime_timer <= 0:
|
||||
if is_explosive:
|
||||
explode(global_position)
|
||||
else:
|
||||
queue_free()
|
||||
return
|
||||
|
||||
travel_time += delta
|
||||
|
||||
# Сохраняем предыдущую позицию для raycast (если понадобится)
|
||||
previous_position = global_position
|
||||
|
||||
# Применяем гравитацию
|
||||
velocity.y -= projectile_gravity * delta
|
||||
|
||||
# Движение снаряда
|
||||
global_position += velocity * delta
|
||||
|
||||
# Проверяем, достигли ли цели
|
||||
# Проверяем по Y координате (приземление) и по времени полета
|
||||
# НЕ проверяем горизонтальное расстояние, так как это может сработать сразу при близких целях
|
||||
if (global_position.y <= target_position.y + 0.3) and (travel_time >= total_time * 0.5):
|
||||
# Включаем коллизию только при приземлении
|
||||
monitoring = true
|
||||
# Устанавливаем позицию точно в целевую точку
|
||||
global_position = target_position
|
||||
on_hit(target_position)
|
||||
return
|
||||
|
||||
# Дополнительная проверка по времени полета (на случай, если снаряд не достиг земли)
|
||||
if travel_time >= total_time * 1.2:
|
||||
monitoring = true
|
||||
global_position = target_position
|
||||
on_hit(target_position)
|
||||
return
|
||||
|
||||
# Не проверяем столкновения при полете - только движение
|
||||
1
scripts/parabolic_projectile.gd.uid
Normal file
1
scripts/parabolic_projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d3c5g160kh3vv
|
||||
249
scripts/player.gd
Normal file
249
scripts/player.gd
Normal file
@@ -0,0 +1,249 @@
|
||||
extends Unit
|
||||
class_name Player
|
||||
|
||||
# Класс игрока с управлением WASD
|
||||
|
||||
@export var mouse_sensitivity: float = 0.003
|
||||
@export var dash_speed: float = 25.0 # Скорость рывка
|
||||
@export var dash_duration: float = 0.2 # Длительность рывка
|
||||
@export var dash_cooldown: float = 1.0 # Перезарядка рывка
|
||||
@export var straight_projectile_scene: PackedScene = null # Сцена прямого снаряда
|
||||
@export var parabolic_projectile_scene: PackedScene = null # Сцена параболического снаряда
|
||||
@export var projectile_damage: float = 20.0
|
||||
@export var projectile_speed: float = 20.0
|
||||
@export var explosion_radius: float = 3.0
|
||||
|
||||
var camera_angle: float = 0.0
|
||||
var is_dashing: bool = false
|
||||
var dash_timer: float = 0.0
|
||||
var dash_cooldown_timer: float = 0.0
|
||||
var dash_direction: Vector3 = Vector3.ZERO
|
||||
|
||||
signal dash_started
|
||||
signal dash_ended
|
||||
signal dash_cooldown_changed(remaining_time: float)
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
# Игрок белый
|
||||
var mesh_instance = get_node_or_null("MeshInstance3D")
|
||||
if mesh_instance:
|
||||
var material = StandardMaterial3D.new()
|
||||
material.albedo_color = Color.WHITE
|
||||
mesh_instance.material_override = material
|
||||
|
||||
func _input(event):
|
||||
# Обработка рывка
|
||||
if event.is_action_pressed("dash") and dash_cooldown_timer <= 0.0 and not is_dashing:
|
||||
perform_dash()
|
||||
|
||||
# Обработка стрельбы (левая кнопка мыши)
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
shoot_straight_projectile()
|
||||
|
||||
# Обработка броска (правая кнопка мыши)
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||
throw_parabolic_projectile()
|
||||
|
||||
func _physics_process(delta):
|
||||
# Обновляем таймеры
|
||||
var previous_cooldown = dash_cooldown_timer
|
||||
if dash_cooldown_timer > 0.0:
|
||||
dash_cooldown_timer -= delta
|
||||
dash_cooldown_timer = max(0.0, dash_cooldown_timer)
|
||||
# Эмитим сигнал только если значение изменилось
|
||||
if abs(previous_cooldown - dash_cooldown_timer) > 0.01:
|
||||
dash_cooldown_changed.emit(dash_cooldown_timer)
|
||||
|
||||
# Поворачиваем игрока в сторону курсора мыши
|
||||
update_rotation_to_mouse()
|
||||
|
||||
# Получаем направление движения от клавиатуры
|
||||
var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
var direction = Vector3(input_dir.x, 0, input_dir.y)
|
||||
|
||||
# Обработка рывка
|
||||
if is_dashing:
|
||||
dash_timer -= delta
|
||||
if dash_timer <= 0.0:
|
||||
end_dash()
|
||||
else:
|
||||
# Во время рывка двигаемся с увеличенной скоростью
|
||||
velocity.x = dash_direction.x * dash_speed
|
||||
velocity.z = dash_direction.z * dash_speed
|
||||
else:
|
||||
# Обычное движение
|
||||
if direction.length() > 0:
|
||||
velocity.x = direction.x * speed
|
||||
velocity.z = direction.z * speed
|
||||
else:
|
||||
velocity.x = move_toward(velocity.x, 0, speed)
|
||||
velocity.z = move_toward(velocity.z, 0, speed)
|
||||
|
||||
# Применяем гравитацию
|
||||
if not is_on_floor():
|
||||
velocity.y -= 9.8 * delta
|
||||
else:
|
||||
velocity.y = 0
|
||||
|
||||
super._physics_process(delta)
|
||||
|
||||
func update_rotation_to_mouse():
|
||||
# Получаем камеру из сцены
|
||||
var camera = get_viewport().get_camera_3d()
|
||||
if not camera:
|
||||
return
|
||||
|
||||
# Получаем позицию курсора мыши на экране
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
|
||||
# Создаем луч от камеры через позицию курсора
|
||||
var from = camera.project_ray_origin(mouse_pos)
|
||||
var to = from + camera.project_ray_normal(mouse_pos) * 1000.0
|
||||
|
||||
# Создаем запрос для raycast
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 1 # Слой земли
|
||||
|
||||
# Выполняем raycast
|
||||
var result = space_state.intersect_ray(query)
|
||||
|
||||
if result:
|
||||
# Получаем точку пересечения
|
||||
var target_point = result.position
|
||||
# Поворачиваем игрока в сторону курсора (только по оси Y)
|
||||
var look_direction = (target_point - global_position)
|
||||
look_direction.y = 0 # Игнорируем вертикальную составляющую
|
||||
if look_direction.length() > 0.1:
|
||||
look_at(global_position + look_direction, Vector3.UP)
|
||||
|
||||
func perform_dash():
|
||||
# Получаем направление движения
|
||||
var input_dir = Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
var direction = Vector3(input_dir.x, 0, input_dir.y)
|
||||
|
||||
# Если игрок не двигается, рывок вперед (по направлению взгляда или вверх)
|
||||
if direction.length() < 0.1:
|
||||
direction = Vector3(0, 0, -1) # Вперед по умолчанию
|
||||
|
||||
# Нормализуем направление
|
||||
dash_direction = direction.normalized()
|
||||
|
||||
# Запускаем рывок
|
||||
is_dashing = true
|
||||
dash_timer = dash_duration
|
||||
dash_cooldown_timer = dash_cooldown
|
||||
dash_started.emit()
|
||||
|
||||
func end_dash():
|
||||
is_dashing = false
|
||||
dash_timer = 0.0
|
||||
dash_ended.emit()
|
||||
|
||||
func shoot_straight_projectile():
|
||||
if not straight_projectile_scene:
|
||||
return
|
||||
|
||||
# Получаем точку прицеливания через raycast
|
||||
var target_point = get_mouse_target_point()
|
||||
if not target_point:
|
||||
return
|
||||
|
||||
# Создаем снаряд
|
||||
var projectile = straight_projectile_scene.instantiate() as StraightProjectile
|
||||
if not projectile:
|
||||
return
|
||||
|
||||
# Устанавливаем параметры
|
||||
projectile.owner_unit = self
|
||||
projectile.damage = projectile_damage
|
||||
projectile.speed = projectile_speed
|
||||
projectile.is_explosive = false
|
||||
|
||||
# Направление полета
|
||||
var shoot_position = global_position + Vector3.UP * 1.0 # Немного выше игрока
|
||||
var direction = (target_point - shoot_position).normalized()
|
||||
projectile.direction = direction
|
||||
|
||||
# Позиция и поворот
|
||||
projectile.global_position = shoot_position
|
||||
projectile.look_at(shoot_position + direction, Vector3.UP)
|
||||
|
||||
# Добавляем в сцену
|
||||
var scene_root = get_tree().current_scene
|
||||
if not scene_root:
|
||||
scene_root = get_tree().root.get_child(get_tree().root.get_child_count() - 1)
|
||||
scene_root.add_child(projectile)
|
||||
|
||||
func throw_parabolic_projectile():
|
||||
if not parabolic_projectile_scene:
|
||||
# Пробуем загрузить вручную
|
||||
parabolic_projectile_scene = load("res://scenes/parabolic_projectile.tscn") as PackedScene
|
||||
if not parabolic_projectile_scene:
|
||||
return
|
||||
|
||||
# Получаем точку прицеливания через raycast (на плоскости)
|
||||
var target_point = get_mouse_target_point()
|
||||
|
||||
# Убеждаемся, что целевая точка на уровне земли (Y = 0)
|
||||
# Если кликнули на противника или другой объект, используем его X и Z, но Y ставим на землю
|
||||
target_point.y = 0.0
|
||||
|
||||
# Позиция запуска
|
||||
var throw_position = global_position + Vector3.UP * 1.0
|
||||
|
||||
# Создаем снаряд
|
||||
var projectile = parabolic_projectile_scene.instantiate()
|
||||
if not projectile:
|
||||
return
|
||||
|
||||
# Устанавливаем параметры ДО добавления в сцену
|
||||
projectile.owner_unit = self
|
||||
projectile.damage = projectile_damage
|
||||
projectile.speed = projectile_speed
|
||||
projectile.is_explosive = true
|
||||
projectile.explosion_radius = explosion_radius
|
||||
|
||||
# Устанавливаем позицию
|
||||
projectile.global_position = throw_position
|
||||
|
||||
# Целевая позиция (на уровне земли) - устанавливаем ДО _ready()
|
||||
projectile.target_position = target_point
|
||||
|
||||
# Добавляем в сцену (после установки всех параметров)
|
||||
var scene_root = get_tree().current_scene
|
||||
if not scene_root:
|
||||
scene_root = get_tree().root.get_child(get_tree().root.get_child_count() - 1)
|
||||
scene_root.add_child(projectile)
|
||||
|
||||
func get_mouse_target_point() -> Vector3:
|
||||
# Получаем камеру
|
||||
var camera = get_viewport().get_camera_3d()
|
||||
if not camera:
|
||||
return global_position + transform.basis.z * -10.0
|
||||
|
||||
# Получаем позицию курсора мыши
|
||||
var mouse_pos = get_viewport().get_mouse_position()
|
||||
|
||||
# Создаем луч от камеры
|
||||
var from = camera.project_ray_origin(mouse_pos)
|
||||
var ray_dir = camera.project_ray_normal(mouse_pos)
|
||||
var to = from + ray_dir * 1000.0
|
||||
|
||||
# Raycast
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 1
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
return result.position
|
||||
|
||||
# Если не попали ни во что, вычисляем точку на плоскости земли
|
||||
if ray_dir.y < -0.01: # Луч направлен вниз
|
||||
var t = -from.y / ray_dir.y
|
||||
return from + ray_dir * t
|
||||
|
||||
# Если луч не направлен вниз, используем точку перед игроком
|
||||
return global_position + transform.basis.z * -10.0
|
||||
1
scripts/player.gd.uid
Normal file
1
scripts/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bt4nfjnngh21i
|
||||
182
scripts/projectile.gd
Normal file
182
scripts/projectile.gd
Normal file
@@ -0,0 +1,182 @@
|
||||
extends Area3D
|
||||
class_name Projectile
|
||||
|
||||
# Базовый класс для снарядов
|
||||
|
||||
@export var speed: float = 120.0
|
||||
@export var damage: float = 20.0
|
||||
@export var lifetime: float = 5.0
|
||||
@export var is_explosive: bool = false
|
||||
@export var explosion_radius: float = 3.0
|
||||
|
||||
var direction: Vector3 = Vector3.ZERO
|
||||
var velocity: Vector3 = Vector3.ZERO
|
||||
var owner_unit: Node3D = null
|
||||
var lifetime_timer: float = 0.0
|
||||
var has_hit: bool = false
|
||||
var previous_position: Vector3 = Vector3.ZERO
|
||||
|
||||
signal hit(target: Node3D, position: Vector3)
|
||||
signal exploded(position: Vector3)
|
||||
|
||||
func _ready():
|
||||
lifetime_timer = lifetime
|
||||
previous_position = global_position
|
||||
# Настраиваем Area3D для обнаружения целей
|
||||
body_entered.connect(_on_body_entered)
|
||||
# Отключаем мониторинг по умолчанию для параболических
|
||||
monitoring = true
|
||||
monitorable = false
|
||||
|
||||
func _physics_process(delta):
|
||||
if has_hit:
|
||||
return
|
||||
|
||||
lifetime_timer -= delta
|
||||
if lifetime_timer <= 0:
|
||||
if is_explosive:
|
||||
explode(global_position)
|
||||
else:
|
||||
queue_free()
|
||||
return
|
||||
|
||||
# Сохраняем предыдущую позицию для raycast
|
||||
previous_position = global_position
|
||||
|
||||
# Движение снаряда
|
||||
global_position += velocity * delta
|
||||
|
||||
# Проверка столкновений через raycast
|
||||
check_collisions_raycast()
|
||||
|
||||
func check_collisions_raycast():
|
||||
# Используем raycast для проверки столкновений
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(previous_position, global_position)
|
||||
query.exclude = [self, owner_unit] if owner_unit else [self]
|
||||
query.collision_mask = 1 # Только слой земли и объектов
|
||||
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
var collider = result.collider
|
||||
var hit_position = result.position
|
||||
|
||||
# Если это враг, наносим урон
|
||||
if collider.has_method("take_damage"):
|
||||
collider.take_damage(damage)
|
||||
hit.emit(collider, hit_position)
|
||||
|
||||
on_hit(hit_position)
|
||||
|
||||
func _on_body_entered(body: Node3D):
|
||||
# Дополнительная проверка через Area3D
|
||||
if has_hit:
|
||||
return
|
||||
|
||||
if body == owner_unit:
|
||||
return
|
||||
|
||||
if body is Projectile:
|
||||
return
|
||||
|
||||
if body.has_method("take_damage"):
|
||||
body.take_damage(damage)
|
||||
hit.emit(body, global_position)
|
||||
|
||||
on_hit(global_position)
|
||||
|
||||
func on_hit(position: Vector3):
|
||||
if has_hit:
|
||||
return
|
||||
|
||||
has_hit = true
|
||||
velocity = Vector3.ZERO
|
||||
# Останавливаем частицы
|
||||
var fire_particles = get_node_or_null("FireParticles")
|
||||
var trail_particles = get_node_or_null("TrailParticles")
|
||||
if fire_particles:
|
||||
fire_particles.emitting = false
|
||||
if trail_particles:
|
||||
trail_particles.emitting = false
|
||||
|
||||
if is_explosive:
|
||||
explode(position)
|
||||
else:
|
||||
# Простое попадание
|
||||
hit.emit(null, position)
|
||||
# Воспроизводим анимацию попадания
|
||||
play_hit_animation()
|
||||
# Удаляем через небольшую задержку
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
queue_free()
|
||||
|
||||
func explode(position: Vector3):
|
||||
# Находим все цели в радиусе взрыва
|
||||
var space_state = get_world_3d().direct_space_state
|
||||
var query = PhysicsShapeQueryParameters3D.new()
|
||||
var sphere_shape = SphereShape3D.new()
|
||||
sphere_shape.radius = explosion_radius
|
||||
query.shape = sphere_shape
|
||||
query.transform.origin = position
|
||||
query.collision_mask = 1
|
||||
|
||||
var results = space_state.intersect_shape(query)
|
||||
|
||||
# Наносим урон всем в радиусе
|
||||
for result in results:
|
||||
var collider = result.collider
|
||||
if collider != owner_unit and collider.has_method("take_damage"):
|
||||
var distance = position.distance_to(collider.global_position)
|
||||
var damage_multiplier = 1.0 - (distance / explosion_radius)
|
||||
collider.take_damage(damage * damage_multiplier)
|
||||
|
||||
exploded.emit(position)
|
||||
play_explosion_animation()
|
||||
# Ждем, пока частицы взрыва отыграют (0.5 секунды)
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
queue_free()
|
||||
|
||||
func play_hit_animation():
|
||||
# Создаем простой визуальный эффект попадания
|
||||
var mesh = get_node_or_null("MeshInstance3D")
|
||||
if mesh:
|
||||
# Увеличиваем размер и скрываем
|
||||
var tween = create_tween()
|
||||
tween.tween_property(mesh, "scale", Vector3(2.0, 2.0, 2.0), 0.2)
|
||||
tween.tween_callback(func(): mesh.visible = false)
|
||||
|
||||
func play_explosion_animation():
|
||||
# Скрываем основной меш
|
||||
var mesh = get_node_or_null("MeshInstance3D")
|
||||
if mesh:
|
||||
mesh.visible = false
|
||||
|
||||
# Останавливаем частицы полета
|
||||
var fire_particles = get_node_or_null("FireParticles")
|
||||
var trail_particles = get_node_or_null("TrailParticles")
|
||||
if fire_particles:
|
||||
fire_particles.emitting = false
|
||||
if trail_particles:
|
||||
trail_particles.emitting = false
|
||||
|
||||
# Воспроизводим частицы взрыва
|
||||
var explosion_particles = get_node_or_null("ExplosionParticles")
|
||||
var explosion_core = get_node_or_null("ExplosionCore")
|
||||
var explosion_smoke = get_node_or_null("ExplosionSmoke")
|
||||
|
||||
# Масштабируем узлы частиц в зависимости от радиуса взрыва
|
||||
# Это создаст визуальный эффект взрыва нужного размера
|
||||
var scale_factor = explosion_radius / 3.0 # 3.0 - базовый радиус взрыва
|
||||
|
||||
if explosion_particles:
|
||||
explosion_particles.scale = Vector3.ONE * scale_factor
|
||||
explosion_particles.restart()
|
||||
explosion_particles.emitting = true
|
||||
if explosion_core:
|
||||
explosion_core.scale = Vector3.ONE * scale_factor * 0.6
|
||||
explosion_core.restart()
|
||||
explosion_core.emitting = true
|
||||
if explosion_smoke:
|
||||
explosion_smoke.scale = Vector3.ONE * scale_factor * 1.2
|
||||
explosion_smoke.restart()
|
||||
explosion_smoke.emitting = true
|
||||
1
scripts/projectile.gd.uid
Normal file
1
scripts/projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bg1bpdny7eta6
|
||||
13
scripts/straight_projectile.gd
Normal file
13
scripts/straight_projectile.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
extends Projectile
|
||||
class_name StraightProjectile
|
||||
|
||||
# Прямой снаряд для стрельбы
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
# Устанавливаем скорость только горизонтально (параллельно земле)
|
||||
var horizontal_dir = direction
|
||||
horizontal_dir.y = 0
|
||||
horizontal_dir = horizontal_dir.normalized()
|
||||
velocity = horizontal_dir * speed
|
||||
|
||||
1
scripts/straight_projectile.gd.uid
Normal file
1
scripts/straight_projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://crsltf0vxq1xd
|
||||
30
scripts/unit.gd
Normal file
30
scripts/unit.gd
Normal file
@@ -0,0 +1,30 @@
|
||||
extends CharacterBody3D
|
||||
class_name Unit
|
||||
|
||||
# Базовый класс для всех юнитов (игрок и противники)
|
||||
|
||||
@export var speed: float = 5.0
|
||||
@export var health: float = 100.0
|
||||
@export var max_health: float = 100.0
|
||||
|
||||
signal health_changed(new_health: float)
|
||||
signal died
|
||||
|
||||
func _ready():
|
||||
health = max_health
|
||||
|
||||
func take_damage(amount: float):
|
||||
health -= amount
|
||||
health = max(0, health)
|
||||
health_changed.emit(health)
|
||||
|
||||
if health <= 0:
|
||||
die()
|
||||
|
||||
func die():
|
||||
died.emit()
|
||||
queue_free()
|
||||
|
||||
func _physics_process(delta):
|
||||
# Базовая физика движения
|
||||
move_and_slide()
|
||||
1
scripts/unit.gd.uid
Normal file
1
scripts/unit.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cok6ep4d2e6mf
|
||||
Reference in New Issue
Block a user