- палитра целиком (индиго, пурпур, ледяная бирюза): SOLID/NEON, SURFACES/GLOWS, туман, ambient, свет запекания, ворота, полосы, надписи; новое небо - запекание 384 сэмпла + огни у подножия башни - вентилятор: VENT_MAX_RISE 16, VENT_ACCEL 80; шахта-лифт без знака и без столба/света (hidden) - музыка Race 2: Poloz «On My Way to Hell» с 1:40 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1023 lines
39 KiB
GDScript
1023 lines
39 KiB
GDScript
extends CharacterBody3D
|
|
|
|
# --- Скорость и инерция (паркур-ощущение) ---
|
|
const WALK_SPEED = 6.0
|
|
const SPRINT_SPEED = 13.0
|
|
const GROUND_ACCEL = 55.0 # Разгон на земле
|
|
const GROUND_FRICTION = 45.0 # Торможение на земле без ввода
|
|
const OVERSPEED_DECEL = 14.0 # Как быстро гасится "лишняя" скорость (после слайда/прыжка) при беге
|
|
const AIR_ACCEL = 18.0 # Управление в воздухе
|
|
const MAX_SPEED = 22.0 # Потолок скорости
|
|
|
|
# --- Прыжок ---
|
|
const JUMP_VELOCITY = 6.0
|
|
const FALL_GRAVITY_MULT = 1.4 # Быстрее падаем — прыжок «злее»
|
|
const JUMP_CUT_GRAVITY_MULT = 2.5 # Отпустил пробел — прыжок ниже
|
|
const COYOTE_TIME = 0.12 # Можно прыгнуть сразу после схода с края
|
|
const JUMP_BUFFER_TIME = 0.12 # Нажал пробел чуть раньше приземления — прыжок сработает
|
|
const SLIDE_JUMP_BOOST = 1.15 # Бонус скорости при прыжке из слайда
|
|
|
|
# --- Двойной прыжок ---
|
|
const AIR_JUMPS = 1 # Сколько прыжков в воздухе (0 — отключить)
|
|
const AIR_JUMP_VELOCITY = 5.5
|
|
|
|
# --- Рывок (клавиша Q) ---
|
|
const DASH_SPEED = 20.0 # Не больше MAX_SPEED
|
|
const DASH_TIME = 0.18 # Длительность рывка
|
|
const DASH_EXIT_SPEED = 13.0 # Скорость, с которой выходим из рывка
|
|
const DASH_COOLDOWN = 0.6
|
|
const MAX_DASHES = 1 # Зарядов рывка в воздухе (восстанавливаются на земле и на стенах)
|
|
const DASH_FOV_BONUS = 16.0
|
|
const DASH_VIGNETTE = 0.5
|
|
|
|
# --- Бег по стенам ---
|
|
const WALLRUN_MIN_SPEED = 6.0 # Минимальная скорость, чтобы «зацепиться» за стену
|
|
const WALLRUN_SPEED = 11.0 # Скорость бега по стене
|
|
const WALLRUN_MAX_TIME = 1.8 # Сколько можно бежать по стене
|
|
const WALLRUN_MAX_FALL_SPEED = 10.0 # Если падаем быстрее — зацепиться нельзя
|
|
const WALLRUN_GRAVITY = 25.0 # Как быстро гасится вертикальная скорость на стене
|
|
const WALLRUN_SINK = 0.8 # Медленное сползание вниз
|
|
const WALLRUN_STICK = 3.0 # Прижимание к стене
|
|
const WALLRUN_COOLDOWN = 0.35 # Пауза после схода со стены
|
|
const WALLRUN_ROLL = 12.0 # Наклон камеры на стене (градусы)
|
|
const WALLRUN_FOV_BONUS = 8.0
|
|
const WALLRUN_VIGNETTE = 0.3
|
|
|
|
# --- Прыжок от стены ---
|
|
const WALL_JUMP_PUSH = 8.0 # Отталкивание от стены
|
|
const WALL_JUMP_UP = 7.0
|
|
const MAX_WALL_JUMPS = 2 # Прыжков от стены без бега по ней (сбрасывается на земле)
|
|
|
|
# --- Подтягивание на уступ (держи пробел в воздухе рядом с краем) ---
|
|
const MANTLE_WALL_Y = 0.5 # Высота (от ног), на которой ищем стену перед собой
|
|
const MANTLE_MIN_HEIGHT = 0.5 # Минимальная высота уступа относительно ног
|
|
const MANTLE_MAX_HEIGHT = 1.5 # Максимальная высота уступа относительно ног
|
|
const MANTLE_REACH = 0.9 # Дальность поиска стены
|
|
const MANTLE_FORWARD = 0.5 # Насколько вглубь уступа ставим персонажа
|
|
const MANTLE_CLEARANCE = 1.8 # Свободное место над уступом (высота капсулы)
|
|
const MANTLE_TIME = 0.4
|
|
const MANTLE_EXIT_SPEED = 5.0
|
|
const MANTLE_MAX_RISE_SPEED = 3.0 # Хватаемся, когда уже не летим резко вверх
|
|
|
|
# --- Приземление ---
|
|
const LAND_MIN_SPEED = 7.0 # С какой скорости падения камера «проседает»
|
|
const HARD_LAND_SPEED = 16.0 # Жёсткое приземление — теряем часть скорости
|
|
const HARD_LAND_SLOWDOWN = 0.6
|
|
|
|
# --- Подкат ---
|
|
const SLIDE_START_SPEED = 15.0 # Минимальная стартовая скорость слайда
|
|
const SLIDE_START_MULT = 1.25 # Множитель от скорости бега
|
|
const SLIDE_FRICTION = 6.0 # Чем меньше — тем дальше слайд
|
|
const SLIDE_END_SPEED = 6.5 # При какой скорости слайд заканчивается
|
|
const SLIDE_STEER = 1.8 # Насколько можно рулить во время слайда
|
|
const SLIDE_COOLDOWN = 0.3
|
|
const SLIDE_CAMERA_DROP = 0.6
|
|
const SLIDE_HEIGHT_FACTOR = 0.5
|
|
const CROUCH_SPEED = 2.5 # Ползём, если над головой низкий потолок
|
|
const CROUCH_WALK_SPEED = 3.5 # Скорость в приседе (держим C; на бегу Shift+C — подкат)
|
|
const CROUCH_CAMERA_DROP = 0.6
|
|
const MIN_SLIDE_SPEED = 7.0 # Минимальная скорость для начала слайда
|
|
|
|
# --- Ступеньки ---
|
|
const STEP_HEIGHT = 0.45 # Максимальная высота ступеньки, на которую поднимаемся сами
|
|
const STEP_PROBE = 0.12 # Минимальная дальность проверки препятствия перед собой
|
|
const STEP_CAMERA_SMOOTH = 14.0 # Плавность камеры при подъёме (больше — резче)
|
|
|
|
# --- Тело (для рейкастов; считаем, что начало координат персонажа у ног) ---
|
|
const BODY_MID_HEIGHT = 0.9 # Высота «пояса» для поиска стен
|
|
const WALL_CHECK_DIST = 0.9 # Дальность поиска стен сбоку
|
|
|
|
# --- Камера ---
|
|
const ZOOM_FOV = 35.0
|
|
const ZOOM_SPEED = 10.0
|
|
const SPRINT_FOV_BONUS = 7.0 # Расширение FOV на скорости
|
|
const SLIDE_FOV_BONUS = 13.0
|
|
const STRAFE_ROLL = 3.0 # Наклон камеры при стрейфе (градусы)
|
|
|
|
# --- Виньетка (затемнение по краям) ---
|
|
const ZOOM_VIGNETTE = 0.85
|
|
const SLIDE_VIGNETTE = 0.45
|
|
const SPRINT_VIGNETTE = 0.2
|
|
const VIGNETTE_SPEED = 8.0
|
|
|
|
# --- Смерть (камень поймал) ---
|
|
const DEATH_TIME = 2.5 # Сколько лежим перед возрождением
|
|
const DEATH_CAMERA_HEIGHT = 0.3 # Камера падает почти на землю
|
|
const DEATH_ROLL = 75.0 # И заваливается набок (градусы)
|
|
|
|
const VIGNETTE_SHADER = """
|
|
shader_type canvas_item;
|
|
uniform float intensity : hint_range(0.0, 1.0) = 0.0;
|
|
void fragment() {
|
|
float d = distance(UV, vec2(0.5));
|
|
float v = smoothstep(0.25, 0.75, d);
|
|
COLOR = vec4(0.0, 0.0, 0.0, v * intensity);
|
|
}
|
|
"""
|
|
|
|
# Настройки чувствительности мыши
|
|
# --- Внешние эффекты карт (objects/vent.gd, objects/boost_strip.gd): локально, в сеть не уходят ---
|
|
const VENT_ACCEL = 80.0 # Подъёмное ускорение в вентиляционном потоке
|
|
const VENT_MAX_RISE = 16.0 # Поток разгоняет вертикальную скорость не выше этой
|
|
const BOOST_ACCEL = 60.0 # Разгон на ускоряющей полосе (потолок — MAX_SPEED)
|
|
const EFFECT_HOLD = 0.12 # Сколько секунд эффект держится после последнего вызова (область зовёт каждый кадр)
|
|
var vent_timer: float = 0.0
|
|
var boost_timer: float = 0.0
|
|
var boost_dir: Vector3 = Vector3.ZERO
|
|
var boost_speed: float = MAX_SPEED
|
|
|
|
@export var MOUSE_SENSITIVITY: float = 0.003
|
|
|
|
# --- Настройки тряски камеры (Head Bobbing) ---
|
|
const BOB_FREQ = 2.4
|
|
const BOB_AMP = 0.06
|
|
var bob_time: float = 0.0
|
|
|
|
# Состояние подката
|
|
var is_sliding: bool = false
|
|
var is_crouching: bool = false
|
|
var slide_cooldown_timer: float = 0.0
|
|
var slide_dir: Vector3 = Vector3.ZERO
|
|
var slide_speed: float = 0.0
|
|
|
|
# Прыжок
|
|
var coyote_timer: float = 0.0
|
|
var jump_buffer_timer: float = 0.0
|
|
var space_was_down: bool = false
|
|
var air_jumps_left: int = AIR_JUMPS
|
|
var wall_jumps_left: int = MAX_WALL_JUMPS
|
|
|
|
# Рывок
|
|
var is_dashing: bool = false
|
|
var dash_timer: float = 0.0
|
|
var dash_cooldown_timer: float = 0.0
|
|
var dash_dir: Vector3 = Vector3.ZERO
|
|
var dashes_left: int = MAX_DASHES
|
|
|
|
# Бег по стене
|
|
var is_wallrunning: bool = false
|
|
var wallrun_timer: float = 0.0
|
|
var wallrun_cooldown_timer: float = 0.0
|
|
var wall_normal: Vector3 = Vector3.ZERO
|
|
var wall_side: float = 0.0 # +1 стена справа, -1 стена слева
|
|
|
|
# Подтягивание на уступ
|
|
var is_mantling: bool = false
|
|
var mantle_t: float = 0.0
|
|
var mantle_start: Vector3 = Vector3.ZERO
|
|
var mantle_target: Vector3 = Vector3.ZERO
|
|
var mantle_dir: Vector3 = Vector3.ZERO
|
|
|
|
# Камера
|
|
var head_base_pos: Vector3 = Vector3.ZERO
|
|
var head_offset_y: float = 0.0
|
|
var land_offset: float = 0.0
|
|
var step_offset: float = 0.0
|
|
var current_roll: float = 0.0
|
|
var shake_strength: float = 0.0
|
|
var base_fov: float = 75.0
|
|
var is_zooming: bool = false
|
|
|
|
# Виньетка
|
|
var vignette_material: ShaderMaterial = null
|
|
var vignette_rect: ColorRect = null
|
|
var vignette_value: float = 0.0
|
|
|
|
# Смерть
|
|
signal died
|
|
signal respawned
|
|
var is_dead: bool = false
|
|
var death_timer: float = 0.0
|
|
var spawn_transform: Transform3D
|
|
var death_overlay: ColorRect = null
|
|
var death_label: Label = null
|
|
|
|
# Заморозка ввода (режим «Гонка»: отсчёт до старта). Обзор мышью работает, движение и способности — нет.
|
|
var input_frozen: bool = false
|
|
|
|
# Коллайдер (опционально, ищется по имени CollisionShape3D)
|
|
var collision_shape: CollisionShape3D = null
|
|
var capsule: CapsuleShape3D = null
|
|
var default_capsule_height: float = 0.0
|
|
var default_collision_y: float = 0.0
|
|
|
|
@onready var head: Camera3D = $Camera3D
|
|
|
|
func _ready() -> void:
|
|
add_to_group("local_player")
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
head_base_pos = head.position
|
|
base_fov = head.fov
|
|
|
|
# Тело видно в редакторе и отбрасывает тень, но своя камера его не рисует (слой 2)
|
|
for mi: MeshInstance3D in $Model.find_children("*", "MeshInstance3D", true, false):
|
|
mi.layers = 2
|
|
head.cull_mask = 0xFFFFF & ~2
|
|
|
|
# Чтобы при спуске по ступенькам персонаж «прилипал» к полу, а не летел по воздуху
|
|
floor_snap_length = STEP_HEIGHT
|
|
|
|
collision_shape = get_node_or_null("CollisionShape3D")
|
|
if collision_shape and collision_shape.shape is CapsuleShape3D:
|
|
capsule = collision_shape.shape
|
|
default_capsule_height = capsule.height
|
|
default_collision_y = collision_shape.position.y
|
|
|
|
_setup_vignette()
|
|
_setup_death_screen()
|
|
spawn_transform = global_transform
|
|
|
|
func _setup_vignette() -> void:
|
|
var layer := CanvasLayer.new()
|
|
layer.layer = 10
|
|
add_child(layer)
|
|
|
|
var rect := ColorRect.new()
|
|
rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
layer.add_child(rect)
|
|
rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
vignette_rect = rect
|
|
|
|
var shader := Shader.new()
|
|
shader.code = VIGNETTE_SHADER
|
|
vignette_material = ShaderMaterial.new()
|
|
vignette_material.shader = shader
|
|
rect.material = vignette_material
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("ui_cancel"):
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
|
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not Net.input_blocked:
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # (панель ника открыта — мышь нужна на экране)
|
|
|
|
if is_dead:
|
|
return
|
|
|
|
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
|
var zoom_factor := head.fov / base_fov
|
|
rotate_y(-event.relative.x * MOUSE_SENSITIVITY * zoom_factor)
|
|
head.rotate_x(-event.relative.y * MOUSE_SENSITIVITY * zoom_factor)
|
|
head.rotation.x = clamp(head.rotation.x, deg_to_rad(-80), deg_to_rad(80))
|
|
|
|
if input_frozen or Net.input_blocked:
|
|
return
|
|
|
|
if event.is_action_pressed("crouch"):
|
|
_try_start_slide() # Подкат: бег (sprint) + crouch
|
|
elif event.is_action_pressed("dash"):
|
|
_try_dash()
|
|
|
|
# Битовые флаги состояния для сетевой синхронизации (см. Net.FLAG_*)
|
|
func get_net_flags() -> int:
|
|
var flags := 0
|
|
if is_sliding or is_crouching: flags |= Net.FLAG_SLIDING # У призрака присед выглядит как сплюснутая капсула
|
|
if is_wallrunning:
|
|
flags |= Net.FLAG_WALLRUN
|
|
if wall_side > 0.0: flags |= Net.FLAG_WALL_RIGHT
|
|
if is_dashing: flags |= Net.FLAG_DASH
|
|
if is_mantling: flags |= Net.FLAG_MANTLE
|
|
if not is_on_floor() and not is_wallrunning and not is_mantling: flags |= Net.FLAG_AIR
|
|
return flags
|
|
|
|
func _get_input_dir() -> Vector2:
|
|
# Действия привязаны к физическим клавишам (работает в любой раскладке); у них же — сенсорные кнопки
|
|
return Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if is_dead:
|
|
_process_death(delta)
|
|
return
|
|
|
|
if input_frozen or Net.input_blocked: # input_blocked — открыта панель ника (core/overlay.gd), не зависит от гонки
|
|
# Стоим на месте (только падаем, если под ногами пусто)
|
|
velocity.x = 0.0
|
|
velocity.z = 0.0
|
|
if not is_on_floor():
|
|
velocity += get_gravity() * delta
|
|
else:
|
|
velocity.y = 0.0
|
|
move_and_slide()
|
|
return
|
|
|
|
slide_cooldown_timer = maxf(slide_cooldown_timer - delta, 0.0)
|
|
dash_cooldown_timer = maxf(dash_cooldown_timer - delta, 0.0)
|
|
wallrun_cooldown_timer = maxf(wallrun_cooldown_timer - delta, 0.0)
|
|
|
|
var space_down := Input.is_action_pressed("jump")
|
|
|
|
_update_crouch()
|
|
|
|
# --- Таймеры прыжка (coyote time + jump buffer) ---
|
|
if is_on_floor():
|
|
coyote_timer = COYOTE_TIME
|
|
# На земле восстанавливаем все воздушные способности
|
|
air_jumps_left = AIR_JUMPS
|
|
dashes_left = MAX_DASHES
|
|
wall_jumps_left = MAX_WALL_JUMPS
|
|
if is_wallrunning:
|
|
_end_wallrun()
|
|
else:
|
|
coyote_timer = maxf(coyote_timer - delta, 0.0)
|
|
|
|
if space_down and not space_was_down:
|
|
jump_buffer_timer = JUMP_BUFFER_TIME
|
|
else:
|
|
jump_buffer_timer = maxf(jump_buffer_timer - delta, 0.0)
|
|
space_was_down = space_down
|
|
|
|
# Ввод
|
|
var input_dir := _get_input_dir()
|
|
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
|
var has_input := direction != Vector3.ZERO
|
|
|
|
# --- Подтягивание на уступ: полностью забирает управление ---
|
|
if is_mantling:
|
|
_process_mantle(delta)
|
|
_update_camera(delta, input_dir)
|
|
return
|
|
|
|
# --- Рывок: тоже забирает управление, пока длится ---
|
|
if is_dashing:
|
|
dash_timer -= delta
|
|
var cancel_by_jump := jump_buffer_timer > 0.0 and coyote_timer > 0.0
|
|
if dash_timer <= 0.0 or cancel_by_jump:
|
|
_end_dash()
|
|
else:
|
|
velocity = dash_dir * DASH_SPEED
|
|
_update_camera(delta, input_dir)
|
|
_move_and_track(delta)
|
|
return
|
|
|
|
# 1. Гравитация (падение быстрее, короткий прыжок при отпускании пробела)
|
|
if not is_on_floor() and not is_wallrunning:
|
|
var g := get_gravity()
|
|
if velocity.y < 0.0:
|
|
g *= FALL_GRAVITY_MULT
|
|
elif not space_down:
|
|
g *= JUMP_CUT_GRAVITY_MULT
|
|
velocity += g * delta
|
|
|
|
# 2a. Автоподтягивание: держим пробел в воздухе рядом с уступом
|
|
if not is_on_floor() and space_down and not is_wallrunning \
|
|
and velocity.y < MANTLE_MAX_RISE_SPEED:
|
|
if _try_mantle():
|
|
jump_buffer_timer = 0.0
|
|
|
|
# 2b. Прыжок
|
|
if not is_mantling and jump_buffer_timer > 0.0:
|
|
if is_wallrunning:
|
|
_wall_jump(wall_normal)
|
|
elif coyote_timer > 0.0:
|
|
if (not is_sliding and not is_crouching) or _can_stand():
|
|
if is_crouching:
|
|
_end_crouch()
|
|
velocity.y = JUMP_VELOCITY
|
|
jump_buffer_timer = 0.0
|
|
coyote_timer = 0.0
|
|
if is_sliding:
|
|
_end_slide()
|
|
# Прыжок из слайда сохраняет и усиливает скорость
|
|
velocity.x *= SLIDE_JUMP_BOOST
|
|
velocity.z *= SLIDE_JUMP_BOOST
|
|
else:
|
|
# Мы в воздухе: сначала пробуем оттолкнуться от стены, потом двойной прыжок
|
|
if wall_jumps_left > 0 and _try_wall_jump():
|
|
wall_jumps_left -= 1
|
|
elif air_jumps_left > 0 and not (velocity.y < 0.0 and _near_ground()):
|
|
_air_jump(direction)
|
|
|
|
if is_mantling:
|
|
_update_camera(delta, input_dir)
|
|
return
|
|
|
|
# 3. Скорость (Спринт)
|
|
var current_speed := WALK_SPEED
|
|
if is_crouching:
|
|
current_speed = CROUCH_WALK_SPEED
|
|
elif Input.is_action_pressed("sprint"):
|
|
current_speed = SPRINT_SPEED
|
|
|
|
# 4. Бег по стене
|
|
if not is_wallrunning and not is_on_floor() and not is_sliding \
|
|
and wallrun_cooldown_timer <= 0.0 and Input.is_action_pressed("move_forward"):
|
|
_try_start_wallrun()
|
|
if is_wallrunning:
|
|
_process_wallrun(delta)
|
|
|
|
# 5. Движение с инерцией
|
|
var horizontal := Vector3(velocity.x, 0, velocity.z)
|
|
|
|
if is_wallrunning:
|
|
pass # скорость уже задана в _process_wallrun
|
|
elif is_sliding:
|
|
horizontal = _process_slide(delta, direction, horizontal)
|
|
elif is_on_floor():
|
|
var target := direction * current_speed
|
|
var rate := GROUND_ACCEL
|
|
if not has_input:
|
|
rate = GROUND_FRICTION
|
|
elif horizontal.length() > current_speed:
|
|
rate = OVERSPEED_DECEL # плавно гасим лишнюю скорость, не рубим резко
|
|
horizontal = horizontal.move_toward(target, rate * delta)
|
|
else:
|
|
# В воздухе импульс сохраняется, можно только подруливать
|
|
if has_input:
|
|
var speed_cap: float = maxf(horizontal.length(), current_speed)
|
|
horizontal += direction * AIR_ACCEL * delta
|
|
if horizontal.length() > speed_cap:
|
|
horizontal = horizontal.normalized() * speed_cap
|
|
|
|
# Полоса: разгоняет вдоль своего направления, но не выше потолка MAX_SPEED
|
|
if boost_timer > 0.0:
|
|
boost_timer -= delta
|
|
var hs := horizontal.length()
|
|
if hs > 3.0 and horizontal.normalized().dot(boost_dir) > 0.5:
|
|
horizontal = horizontal.normalized() * maxf(hs, minf(hs + BOOST_ACCEL * delta, boost_speed)) # только добирает: быстрее полосы не тормозит
|
|
if horizontal.length() > MAX_SPEED:
|
|
horizontal = horizontal.normalized() * MAX_SPEED
|
|
|
|
velocity.x = horizontal.x
|
|
velocity.z = horizontal.z
|
|
|
|
# Вентиляционный поток: тянет вверх (горизонтальная скорость не трогается), возвращает двойной прыжок
|
|
if vent_timer > 0.0:
|
|
vent_timer -= delta
|
|
velocity.y = minf(velocity.y + VENT_ACCEL * delta, maxf(velocity.y, VENT_MAX_RISE))
|
|
air_jumps_left = AIR_JUMPS
|
|
|
|
# 6. Камера
|
|
_update_camera(delta, input_dir)
|
|
|
|
_move_and_track(delta)
|
|
|
|
# Вызывает область-вентилятор каждый физический кадр, пока игрок в потоке
|
|
func apply_vent() -> void:
|
|
vent_timer = EFFECT_HOLD
|
|
|
|
# Вызывает ускоряющая полоса каждый кадр: dir — куда она разгоняет (горизонтально), speed — до какой скорости
|
|
func apply_boost(dir: Vector3, speed: float) -> void:
|
|
boost_timer = EFFECT_HOLD
|
|
boost_dir = Vector3(dir.x, 0.0, dir.z).normalized()
|
|
boost_speed = minf(speed, MAX_SPEED)
|
|
|
|
# Двигаемся и отслеживаем приземление
|
|
func _move_and_track(delta: float) -> void:
|
|
var pre_vy := velocity.y
|
|
var was_on_floor := is_on_floor()
|
|
if was_on_floor and velocity.y <= 0.1 and not is_wallrunning:
|
|
_try_step_up(delta)
|
|
move_and_slide()
|
|
if is_on_floor() and not was_on_floor and pre_vy < -LAND_MIN_SPEED:
|
|
_on_land(-pre_vy)
|
|
|
|
# Автоматический подъём на ступеньки: если перед нами невысокое препятствие,
|
|
# приподнимаем персонажа ровно настолько, чтобы он встал на его верх.
|
|
func _try_step_up(delta: float) -> void:
|
|
var horiz := Vector3(velocity.x, 0, velocity.z)
|
|
if horiz.length() < 0.5:
|
|
return
|
|
|
|
var motion := horiz.normalized() * maxf(horiz.length() * delta, STEP_PROBE)
|
|
var from := global_transform
|
|
|
|
# 1. Перед нами вообще есть препятствие?
|
|
if not test_move(from, motion):
|
|
return
|
|
|
|
# 2. Над нами достаточно места, чтобы приподняться?
|
|
var up := Vector3.UP * STEP_HEIGHT
|
|
if test_move(from, up):
|
|
return
|
|
var raised := from.translated(up)
|
|
|
|
# 3. Если и на высоте ступеньки путь закрыт — это стена, а не ступенька
|
|
if test_move(raised, motion):
|
|
return
|
|
|
|
# 4. Опускаемся вниз за препятствием и ищем, на какой высоте там пол
|
|
var down_hit := KinematicCollision3D.new()
|
|
var forward_pos := raised.translated(motion)
|
|
if not test_move(forward_pos, Vector3.DOWN * STEP_HEIGHT, down_hit):
|
|
return # пола там нет — это обрыв, а не ступенька
|
|
if down_hit.get_normal().y < cos(floor_max_angle):
|
|
return # слишком крутая поверхность
|
|
|
|
var rise := STEP_HEIGHT - down_hit.get_travel().length()
|
|
if rise > 0.01:
|
|
global_position.y += rise + 0.005
|
|
step_offset -= rise # камера плавно догоняет, без рывка
|
|
|
|
func _on_land(fall_speed: float) -> void:
|
|
# Камера «проседает» сильнее при высоком падении
|
|
land_offset = -clampf((fall_speed - LAND_MIN_SPEED) * 0.03, 0.05, 0.45)
|
|
shake_strength = maxf(shake_strength, clampf(fall_speed * 0.004, 0.02, 0.1))
|
|
# Жёсткое приземление гасит скорость (если не перешли в подкат)
|
|
if fall_speed > HARD_LAND_SPEED and not is_sliding:
|
|
velocity.x *= HARD_LAND_SLOWDOWN
|
|
velocity.z *= HARD_LAND_SLOWDOWN
|
|
|
|
# --- РАЙКАСТЫ ---
|
|
|
|
func _ray(from: Vector3, to: Vector3) -> Dictionary:
|
|
var query := PhysicsRayQueryParameters3D.create(from, to, collision_mask, [get_rid()])
|
|
return get_world_3d().direct_space_state.intersect_ray(query)
|
|
|
|
func _near_ground() -> bool:
|
|
var from := global_position + Vector3.UP * 0.1
|
|
return not _ray(from, global_position + Vector3.DOWN * 0.8).is_empty()
|
|
|
|
# --- ДВОЙНОЙ ПРЫЖОК ---
|
|
|
|
func _air_jump(direction: Vector3) -> void:
|
|
velocity.y = AIR_JUMP_VELOCITY
|
|
air_jumps_left -= 1
|
|
jump_buffer_timer = 0.0
|
|
# Можно сменить направление полёта прямо в прыжке
|
|
if direction != Vector3.ZERO:
|
|
var h_speed: float = maxf(Vector2(velocity.x, velocity.z).length(), WALK_SPEED)
|
|
velocity.x = direction.x * h_speed
|
|
velocity.z = direction.z * h_speed
|
|
shake_strength = maxf(shake_strength, 0.02)
|
|
|
|
# --- РЫВОК ---
|
|
|
|
func _try_dash() -> void:
|
|
if is_dashing or is_mantling or dashes_left <= 0 or dash_cooldown_timer > 0.0:
|
|
return
|
|
|
|
var look := -head.global_transform.basis.z
|
|
var dir: Vector3
|
|
if is_on_floor():
|
|
# На земле рвёмся по направлению ввода, а без ввода — вперёд
|
|
var input_dir := _get_input_dir()
|
|
if input_dir != Vector2.ZERO:
|
|
dir = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
|
else:
|
|
dir = Vector3(look.x, 0, look.z).normalized()
|
|
else:
|
|
# В воздухе рвёмся туда, куда смотрит камера
|
|
dir = look.normalized()
|
|
|
|
if is_sliding:
|
|
_end_slide()
|
|
if is_wallrunning:
|
|
_end_wallrun()
|
|
|
|
is_dashing = true
|
|
dash_timer = DASH_TIME
|
|
dash_dir = dir
|
|
dashes_left -= 1
|
|
dash_cooldown_timer = DASH_COOLDOWN
|
|
shake_strength = maxf(shake_strength, 0.06)
|
|
|
|
func _end_dash() -> void:
|
|
is_dashing = false
|
|
velocity = dash_dir * DASH_EXIT_SPEED
|
|
velocity.y *= 0.6 # не улетаем в небо после рывка вверх
|
|
|
|
# --- БЕГ ПО СТЕНАМ ---
|
|
|
|
func _try_start_wallrun() -> void:
|
|
var horiz := Vector3(velocity.x, 0, velocity.z)
|
|
if horiz.length() < WALLRUN_MIN_SPEED or velocity.y < -WALLRUN_MAX_FALL_SPEED:
|
|
return
|
|
|
|
var origin := global_position + Vector3.UP * BODY_MID_HEIGHT
|
|
var right := global_transform.basis.x
|
|
var side := 1.0
|
|
var hit := _ray(origin, origin + right * WALL_CHECK_DIST)
|
|
if hit.is_empty():
|
|
side = -1.0
|
|
hit = _ray(origin, origin - right * WALL_CHECK_DIST)
|
|
if hit.is_empty():
|
|
return
|
|
|
|
var n: Vector3 = hit.normal
|
|
if absf(n.y) > 0.3:
|
|
return # это пол или потолок, а не стена
|
|
n.y = 0.0
|
|
|
|
is_wallrunning = true
|
|
wallrun_timer = WALLRUN_MAX_TIME
|
|
wall_normal = n.normalized()
|
|
wall_side = side
|
|
# На стене восстанавливаем двойной прыжок и рывок — можно цепочкой лететь дальше
|
|
air_jumps_left = AIR_JUMPS
|
|
dashes_left = MAX_DASHES
|
|
|
|
func _process_wallrun(delta: float) -> void:
|
|
wallrun_timer -= delta
|
|
var origin := global_position + Vector3.UP * BODY_MID_HEIGHT
|
|
var hit := _ray(origin, origin - wall_normal * (WALL_CHECK_DIST + 0.3))
|
|
var horiz := Vector3(velocity.x, 0, velocity.z)
|
|
|
|
if hit.is_empty() or wallrun_timer <= 0.0 or is_on_floor() \
|
|
or not Input.is_action_pressed("move_forward") \
|
|
or horiz.length() < WALLRUN_MIN_SPEED * 0.6:
|
|
_end_wallrun()
|
|
return
|
|
|
|
var n: Vector3 = hit.normal
|
|
n.y = 0.0
|
|
wall_normal = n.normalized()
|
|
|
|
# Направление вдоль стены — туда, куда мы уже двигались
|
|
var along := wall_normal.cross(Vector3.UP).normalized()
|
|
if along.dot(horiz) < 0.0:
|
|
along = -along
|
|
|
|
var spd: float = clampf(maxf(horiz.dot(along), WALLRUN_SPEED), 0.0, MAX_SPEED)
|
|
velocity.x = along.x * spd - wall_normal.x * WALLRUN_STICK
|
|
velocity.z = along.z * spd - wall_normal.z * WALLRUN_STICK
|
|
velocity.y = move_toward(velocity.y, -WALLRUN_SINK, WALLRUN_GRAVITY * delta)
|
|
|
|
func _end_wallrun() -> void:
|
|
is_wallrunning = false
|
|
wallrun_cooldown_timer = WALLRUN_COOLDOWN
|
|
|
|
# --- ПРЫЖОК ОТ СТЕНЫ ---
|
|
|
|
func _wall_jump(normal: Vector3) -> void:
|
|
var n := Vector3(normal.x, 0, normal.z).normalized()
|
|
var horiz := Vector3(velocity.x, 0, velocity.z)
|
|
horiz -= n * horiz.dot(n) # убираем составляющую, направленную в стену
|
|
var new_h := horiz * 0.9 + n * WALL_JUMP_PUSH
|
|
if new_h.length() > MAX_SPEED:
|
|
new_h = new_h.normalized() * MAX_SPEED
|
|
velocity.x = new_h.x
|
|
velocity.z = new_h.z
|
|
velocity.y = WALL_JUMP_UP
|
|
|
|
if is_wallrunning:
|
|
_end_wallrun()
|
|
wallrun_cooldown_timer = WALLRUN_COOLDOWN + 0.1
|
|
dashes_left = MAX_DASHES
|
|
jump_buffer_timer = 0.0
|
|
shake_strength = maxf(shake_strength, 0.04)
|
|
|
|
func _try_wall_jump() -> bool:
|
|
var origin := global_position + Vector3.UP * BODY_MID_HEIGHT
|
|
var b := global_transform.basis
|
|
var dirs: Array[Vector3] = [b.x, -b.x, -b.z, b.z]
|
|
var best_normal := Vector3.ZERO
|
|
var best_dist := INF
|
|
|
|
for d in dirs:
|
|
var hit := _ray(origin, origin + d * WALL_CHECK_DIST)
|
|
if hit.is_empty():
|
|
continue
|
|
var n: Vector3 = hit.normal
|
|
if absf(n.y) > 0.3:
|
|
continue
|
|
var dist := origin.distance_to(hit.position)
|
|
if dist < best_dist:
|
|
best_dist = dist
|
|
best_normal = n
|
|
|
|
if best_normal == Vector3.ZERO:
|
|
return false
|
|
_wall_jump(best_normal)
|
|
return true
|
|
|
|
# --- ПОДТЯГИВАНИЕ НА УСТУП ---
|
|
|
|
func _try_mantle() -> bool:
|
|
if is_mantling or is_dashing:
|
|
return false
|
|
|
|
var fwd := -global_transform.basis.z
|
|
fwd.y = 0.0
|
|
if fwd.length() < 0.01:
|
|
return false
|
|
fwd = fwd.normalized()
|
|
var base := global_position
|
|
|
|
# 1. Перед нами должна быть стена
|
|
var low_from := base + Vector3.UP * MANTLE_WALL_Y
|
|
var wall := _ray(low_from, low_from + fwd * MANTLE_REACH)
|
|
if wall.is_empty():
|
|
return false
|
|
var wn: Vector3 = wall.normal
|
|
if absf(wn.y) > 0.3:
|
|
return false
|
|
|
|
# 2. На максимальной высоте перед нами должно быть свободно (значит, там край)
|
|
var high_from := base + Vector3.UP * MANTLE_MAX_HEIGHT
|
|
if not _ray(high_from, high_from + fwd * MANTLE_REACH).is_empty():
|
|
return false
|
|
|
|
# 3. Ищем верхнюю поверхность уступа
|
|
var wall_pos: Vector3 = wall.position
|
|
var top_xz := wall_pos - wn * MANTLE_FORWARD
|
|
var down_from := Vector3(top_xz.x, base.y + MANTLE_MAX_HEIGHT + 0.3, top_xz.z)
|
|
var down_len := MANTLE_MAX_HEIGHT + 0.3 - MANTLE_MIN_HEIGHT
|
|
var ledge := _ray(down_from, down_from + Vector3.DOWN * down_len)
|
|
if ledge.is_empty():
|
|
return false
|
|
var ln: Vector3 = ledge.normal
|
|
if ln.y < 0.7:
|
|
return false
|
|
|
|
# 4. Над уступом должно хватать места, чтобы встать
|
|
var ledge_pos: Vector3 = ledge.position
|
|
if not _ray(ledge_pos + Vector3.UP * 0.05, ledge_pos + Vector3.UP * MANTLE_CLEARANCE).is_empty():
|
|
return false
|
|
|
|
# Всё подходит — залезаем
|
|
if is_sliding:
|
|
_end_slide()
|
|
if is_wallrunning:
|
|
_end_wallrun()
|
|
is_mantling = true
|
|
mantle_t = 0.0
|
|
mantle_start = base
|
|
# Центр капсулы — на полвысоты над уступом: иначе тело наполовину в уступе и физика выталкивает назад
|
|
mantle_target = ledge_pos + Vector3.UP * (default_capsule_height * 0.5 + 0.05)
|
|
mantle_dir = fwd
|
|
velocity = Vector3.ZERO
|
|
shake_strength = maxf(shake_strength, 0.03)
|
|
return true
|
|
|
|
func _process_mantle(delta: float) -> void:
|
|
mantle_t += delta / MANTLE_TIME
|
|
var t := clampf(mantle_t, 0.0, 1.0)
|
|
# Сначала подтягиваемся вверх, затем перекатываемся вперёд на уступ
|
|
var up_t := 1.0 - pow(1.0 - clampf(t / 0.6, 0.0, 1.0), 2.0)
|
|
var fwd_t := smoothstep(0.0, 1.0, clampf((t - 0.4) / 0.6, 0.0, 1.0))
|
|
global_position = Vector3(
|
|
lerpf(mantle_start.x, mantle_target.x, fwd_t),
|
|
lerpf(mantle_start.y, mantle_target.y, up_t),
|
|
lerpf(mantle_start.z, mantle_target.z, fwd_t))
|
|
|
|
if mantle_t >= 1.0:
|
|
is_mantling = false
|
|
velocity = mantle_dir * MANTLE_EXIT_SPEED
|
|
dashes_left = MAX_DASHES
|
|
air_jumps_left = AIR_JUMPS
|
|
|
|
# --- ПОДКАТ ---
|
|
|
|
func _try_start_slide() -> void:
|
|
if is_sliding or slide_cooldown_timer > 0.0:
|
|
return
|
|
if is_dashing or is_mantling or is_wallrunning:
|
|
return
|
|
if not is_on_floor():
|
|
return
|
|
if not Input.is_action_pressed("sprint"):
|
|
return
|
|
|
|
var horizontal := Vector3(velocity.x, 0, velocity.z)
|
|
if horizontal.length() < MIN_SLIDE_SPEED:
|
|
return
|
|
|
|
is_crouching = false
|
|
is_sliding = true
|
|
slide_dir = horizontal.normalized()
|
|
slide_speed = clamp(max(horizontal.length() * SLIDE_START_MULT, SLIDE_START_SPEED), 0.0, MAX_SPEED)
|
|
_set_collider_crouched(true)
|
|
|
|
func _process_slide(delta: float, direction: Vector3, horizontal: Vector3) -> Vector3:
|
|
# Сорвались с края во время слайда — выходим, если есть место
|
|
if not is_on_floor() and _can_stand():
|
|
_end_slide()
|
|
return horizontal
|
|
|
|
if slide_speed > SLIDE_END_SPEED:
|
|
# Лёгкое рулёжка во время слайда
|
|
if direction != Vector3.ZERO:
|
|
var new_dir := slide_dir.lerp(direction, SLIDE_STEER * delta)
|
|
if new_dir.length() > 0.01:
|
|
slide_dir = new_dir.normalized()
|
|
slide_speed = move_toward(slide_speed, 0.0, SLIDE_FRICTION * delta)
|
|
return slide_dir * slide_speed
|
|
|
|
# Слайд выдохся
|
|
if _can_stand():
|
|
_end_slide()
|
|
return slide_dir * slide_speed
|
|
|
|
# Под низким потолком — ползём, пока не выберемся
|
|
if direction != Vector3.ZERO:
|
|
return direction * CROUCH_SPEED
|
|
return horizontal.move_toward(Vector3.ZERO, CROUCH_SPEED)
|
|
|
|
# --- ПРИСЕД ---
|
|
|
|
# Держим C на земле — приседаем. Встаём, когда отпустили, и над головой есть место
|
|
func _update_crouch() -> void:
|
|
var busy := is_sliding or is_dashing or is_wallrunning or is_mantling or not is_on_floor()
|
|
if not is_crouching:
|
|
if Input.is_action_pressed("crouch") and not busy:
|
|
is_crouching = true
|
|
_set_collider_crouched(true)
|
|
elif (busy or not Input.is_action_pressed("crouch")) and (is_sliding or _can_stand()):
|
|
_end_crouch()
|
|
|
|
func _end_crouch() -> void:
|
|
is_crouching = false
|
|
if not is_sliding:
|
|
_set_collider_crouched(false)
|
|
|
|
func _end_slide() -> void:
|
|
is_sliding = false
|
|
slide_cooldown_timer = SLIDE_COOLDOWN
|
|
_set_collider_crouched(false)
|
|
|
|
func _set_collider_crouched(crouched: bool) -> void:
|
|
if capsule == null:
|
|
return
|
|
if crouched:
|
|
var new_height := default_capsule_height * SLIDE_HEIGHT_FACTOR
|
|
capsule.height = new_height
|
|
collision_shape.position.y = default_collision_y - (default_capsule_height - new_height) * 0.5
|
|
else:
|
|
capsule.height = default_capsule_height
|
|
collision_shape.position.y = default_collision_y
|
|
|
|
func _can_stand() -> bool:
|
|
if capsule == null:
|
|
return true
|
|
var extra := default_capsule_height * (1.0 - SLIDE_HEIGHT_FACTOR)
|
|
return not test_move(global_transform, Vector3.UP * extra)
|
|
|
|
# --- КАМЕРА И ЭФФЕКТЫ ---
|
|
|
|
func _update_camera(delta: float, input_dir: Vector2) -> void:
|
|
var h_speed := Vector2(velocity.x, velocity.z).length()
|
|
var sprint_factor: float = clamp((h_speed - WALK_SPEED) / (SPRINT_SPEED - WALK_SPEED), 0.0, 1.0)
|
|
|
|
# Покачивание только при обычном беге/ходьбе
|
|
var grounded_walk := is_on_floor() and not is_sliding and not is_mantling and not is_dashing
|
|
if grounded_walk:
|
|
bob_time += delta * h_speed
|
|
else:
|
|
bob_time = 0.0
|
|
var bob := _head_bob(bob_time) if grounded_walk else Vector3.ZERO
|
|
|
|
# Опускание камеры при слайде и подтягивании
|
|
var target_offset := 0.0
|
|
if is_sliding:
|
|
target_offset = -SLIDE_CAMERA_DROP
|
|
elif is_crouching:
|
|
target_offset = -CROUCH_CAMERA_DROP
|
|
elif is_mantling:
|
|
target_offset = -0.25
|
|
head_offset_y = lerp(head_offset_y, target_offset, 12.0 * delta)
|
|
|
|
# «Проседание» камеры при приземлении
|
|
land_offset = lerpf(land_offset, 0.0, 9.0 * delta)
|
|
|
|
# Камера плавно догоняет персонажа после подъёма на ступеньку
|
|
step_offset = lerpf(step_offset, 0.0, STEP_CAMERA_SMOOTH * delta)
|
|
|
|
# Тряска камеры (затухает сама)
|
|
shake_strength = lerpf(shake_strength, 0.0, 10.0 * delta)
|
|
var shake_pos := Vector3(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0), 0.0) * shake_strength
|
|
|
|
head.position = head_base_pos + bob + shake_pos + Vector3(0, head_offset_y + land_offset + step_offset, 0)
|
|
|
|
# Наклон камеры: на стене — от стены, иначе — при стрейфе
|
|
var target_roll := -input_dir.x * deg_to_rad(STRAFE_ROLL) * sprint_factor
|
|
if is_wallrunning:
|
|
# Если наклон получился в сторону стены — просто поменяй знак у wall_side
|
|
target_roll = wall_side * deg_to_rad(WALLRUN_ROLL)
|
|
current_roll = lerpf(current_roll, target_roll, 8.0 * delta)
|
|
head.rotation.z = current_roll + randf_range(-1.0, 1.0) * shake_strength * 0.3
|
|
|
|
# Зум на ЛКМ
|
|
is_zooming = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) \
|
|
and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
|
|
|
|
# FOV: зум > рывок > слайд > стена > скорость бега
|
|
var target_fov := base_fov + sprint_factor * SPRINT_FOV_BONUS
|
|
if is_zooming:
|
|
target_fov = ZOOM_FOV
|
|
elif is_dashing:
|
|
target_fov = base_fov + DASH_FOV_BONUS
|
|
elif is_sliding:
|
|
target_fov = base_fov + SLIDE_FOV_BONUS
|
|
elif is_wallrunning:
|
|
target_fov = base_fov + WALLRUN_FOV_BONUS
|
|
head.fov = lerp(head.fov, target_fov, ZOOM_SPEED * delta)
|
|
|
|
# Виньетка: зум > рывок > слайд > стена > бег
|
|
var target_vignette := sprint_factor * SPRINT_VIGNETTE
|
|
if is_zooming:
|
|
target_vignette = ZOOM_VIGNETTE
|
|
elif is_dashing:
|
|
target_vignette = DASH_VIGNETTE
|
|
elif is_sliding:
|
|
target_vignette = SLIDE_VIGNETTE
|
|
elif is_wallrunning:
|
|
target_vignette = WALLRUN_VIGNETTE
|
|
vignette_value = lerp(vignette_value, target_vignette, VIGNETTE_SPEED * delta)
|
|
if vignette_material:
|
|
vignette_material.set_shader_parameter("intensity", vignette_value)
|
|
# Невидимая виньетка не должна стоить заливки всего экрана (важно для веба)
|
|
vignette_rect.visible = vignette_value > 0.005
|
|
|
|
func _head_bob(time: float) -> Vector3:
|
|
var pos := Vector3.ZERO
|
|
pos.y = sin(time * BOB_FREQ) * BOB_AMP
|
|
pos.x = cos(time * BOB_FREQ / 2) * BOB_AMP
|
|
return pos
|
|
|
|
# --- СМЕРТЬ И ВОЗРОЖДЕНИЕ ---
|
|
|
|
func _setup_death_screen() -> void:
|
|
var layer := CanvasLayer.new()
|
|
layer.layer = 11
|
|
add_child(layer)
|
|
|
|
death_overlay = ColorRect.new()
|
|
death_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
death_overlay.color = Color(0.6, 0.0, 0.0, 0.0)
|
|
death_overlay.visible = false
|
|
layer.add_child(death_overlay)
|
|
death_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
death_label = Label.new()
|
|
death_label.text = "ПОЙМАЛ!"
|
|
death_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
death_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
death_label.add_theme_font_size_override("font_size", 96)
|
|
death_label.add_theme_color_override("font_color", Color.WHITE)
|
|
death_label.add_theme_constant_override("outline_size", 12)
|
|
death_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
|
death_label.visible = false
|
|
death_overlay.add_child(death_label)
|
|
death_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
|
|
func die() -> void:
|
|
if is_dead:
|
|
return
|
|
is_dead = true
|
|
death_timer = DEATH_TIME
|
|
if is_sliding:
|
|
_end_slide()
|
|
if is_crouching:
|
|
_end_crouch()
|
|
is_dashing = false
|
|
is_wallrunning = false
|
|
is_mantling = false
|
|
velocity = Vector3.ZERO
|
|
shake_strength = 0.25
|
|
death_label.visible = true
|
|
death_overlay.visible = true
|
|
died.emit()
|
|
|
|
func _process_death(delta: float) -> void:
|
|
death_timer -= delta
|
|
|
|
# Камера падает и заваливается набок, экран краснеет
|
|
var k := 1.0 - exp(-6.0 * delta)
|
|
head.position = head.position.lerp(Vector3(head_base_pos.x, DEATH_CAMERA_HEIGHT, head_base_pos.z), k)
|
|
head.rotation.z = lerpf(head.rotation.z, deg_to_rad(DEATH_ROLL), k)
|
|
head.fov = lerpf(head.fov, base_fov, k)
|
|
shake_strength = lerpf(shake_strength, 0.0, 10.0 * delta)
|
|
death_overlay.color.a = lerpf(death_overlay.color.a, 0.55, k)
|
|
|
|
# Падаем на землю, если умерли в воздухе
|
|
velocity.x = 0.0
|
|
velocity.z = 0.0
|
|
if not is_on_floor():
|
|
velocity += get_gravity() * delta
|
|
move_and_slide()
|
|
|
|
if death_timer <= 0.0:
|
|
_respawn()
|
|
|
|
# Поставить игрока в точку t (лицом по её повороту), сбросив скорость и способности. Для карт-режимов (Гонка).
|
|
func place(t: Transform3D, pitch := 0.0) -> void:
|
|
spawn_transform = t
|
|
if is_dead:
|
|
_respawn()
|
|
else:
|
|
if is_sliding:
|
|
_end_slide()
|
|
if is_crouching:
|
|
_end_crouch()
|
|
is_dashing = false
|
|
is_wallrunning = false
|
|
is_mantling = false
|
|
global_transform = t
|
|
velocity = Vector3.ZERO
|
|
head.rotation.x = pitch
|
|
|
|
func _respawn() -> void:
|
|
global_transform = spawn_transform
|
|
velocity = Vector3.ZERO
|
|
head.position = head_base_pos
|
|
head.rotation = Vector3.ZERO
|
|
current_roll = 0.0
|
|
head_offset_y = 0.0
|
|
land_offset = 0.0
|
|
step_offset = 0.0
|
|
death_overlay.color.a = 0.0
|
|
death_label.visible = false
|
|
death_overlay.visible = false
|
|
is_dead = false
|
|
respawned.emit()
|