This commit is contained in:
Redsandy
2026-01-18 20:56:24 +03:00
commit 7980e0add8
68 changed files with 1605294 additions and 0 deletions

View 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)

View File

@@ -0,0 +1 @@
uid://28onm6d36qbo

77
scripts/enemy.gd Normal file
View 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
View File

@@ -0,0 +1 @@
uid://8vuywuv7ebsl

View 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

View File

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

53
scripts/hud.gd Normal file
View 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
View File

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

29
scripts/main_menu.gd Normal file
View 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
View File

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

View 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
# Не проверяем столкновения при полете - только движение

View File

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

249
scripts/player.gd Normal file
View 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
View File

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

182
scripts/projectile.gd Normal file
View 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

View File

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

View 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

View File

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

30
scripts/unit.gd Normal file
View 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
View File

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