diff --git a/Player/character_body_3d.gd b/Player/character_body_3d.gd index 8e04477..077847f 100644 --- a/Player/character_body_3d.gd +++ b/Player/character_body_3d.gd @@ -17,6 +17,53 @@ 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 # Множитель от скорости бега @@ -29,6 +76,15 @@ const SLIDE_HEIGHT_FACTOR = 0.5 const CROUCH_SPEED = 2.5 # Ползём, если над головой низкий потолок 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 @@ -70,10 +126,37 @@ 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 @@ -94,6 +177,9 @@ func _ready() -> void: head_base_pos = head.position base_fov = head.fov + # Чтобы при спуске по ступенькам персонаж «прилипал» к полу, а не летел по воздуху + 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 @@ -131,54 +217,13 @@ func _unhandled_input(event: InputEvent) -> void: 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)) - # Подкат: бег (Shift) + C - if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_C: - _try_start_slide() + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_C: + _try_start_slide() # Подкат: бег (Shift) + C + elif event.keycode == KEY_Q: + _try_dash() # Рывок -func _physics_process(delta: float) -> void: - slide_cooldown_timer = max(slide_cooldown_timer - delta, 0.0) - - var space_down := Input.is_key_pressed(KEY_SPACE) - - # --- Таймеры прыжка (coyote time + jump buffer) --- - if is_on_floor(): - coyote_timer = COYOTE_TIME - else: - coyote_timer = max(coyote_timer - delta, 0.0) - - if space_down and not space_was_down: - jump_buffer_timer = JUMP_BUFFER_TIME - else: - jump_buffer_timer = max(jump_buffer_timer - delta, 0.0) - space_was_down = space_down - - # 1. Гравитация (падение быстрее, короткий прыжок при отпускании пробела) - if not is_on_floor(): - 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 - - # 2. Прыжок - if jump_buffer_timer > 0.0 and coyote_timer > 0.0: - if not is_sliding or _can_stand(): - 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 - - # 3. Скорость (Спринт) - var current_speed := WALK_SPEED - if Input.is_key_pressed(KEY_SHIFT): - current_speed = SPRINT_SPEED - - # 4. Управление WASD +func _get_input_dir() -> Vector2: var input_dir := Vector2.ZERO if Input.is_key_pressed(KEY_D): input_dir.x += 1 @@ -188,15 +233,114 @@ func _physics_process(delta: float) -> void: input_dir.y += 1 if Input.is_key_pressed(KEY_W): input_dir.y -= 1 + return input_dir.normalized() - input_dir = input_dir.normalized() +func _physics_process(delta: float) -> void: + 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) - # 5. Движение с инерцией + var space_down := Input.is_key_pressed(KEY_SPACE) + + # --- Таймеры прыжка (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 horizontal := Vector3(velocity.x, 0, velocity.z) var has_input := direction != Vector3.ZERO - if is_sliding: + # --- Подтягивание на уступ: полностью забирает управление --- + 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 or _can_stand(): + 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 Input.is_key_pressed(KEY_SHIFT): + 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_key_pressed(KEY_W): + _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 @@ -209,7 +353,7 @@ func _physics_process(delta: float) -> void: else: # В воздухе импульс сохраняется, можно только подруливать if has_input: - var speed_cap: float = max(horizontal.length(), current_speed) + var speed_cap: float = maxf(horizontal.length(), current_speed) horizontal += direction * AIR_ACCEL * delta if horizontal.length() > speed_cap: horizontal = horizontal.normalized() * speed_cap @@ -223,13 +367,309 @@ func _physics_process(delta: float) -> void: # 6. Камера _update_camera(delta, input_dir) + _move_and_track(delta) + +# Двигаемся и отслеживаем приземление +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_key_pressed(KEY_W) \ + 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 * 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_key_pressed(KEY_SHIFT): @@ -298,40 +738,67 @@ func _update_camera(delta: float, input_dir: Vector2) -> void: var sprint_factor: float = clamp((h_speed - WALK_SPEED) / (SPRINT_SPEED - WALK_SPEED), 0.0, 1.0) # Покачивание только при обычном беге/ходьбе - if is_sliding or not is_on_floor(): - bob_time = 0.0 - else: + 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 bob := Vector3.ZERO if (is_sliding or not is_on_floor()) else _head_bob(bob_time) - - # Опускание камеры при слайде - var target_offset := -SLIDE_CAMERA_DROP if is_sliding else 0.0 + # Опускание камеры при слайде и подтягивании + var target_offset := 0.0 + if is_sliding: + target_offset = -SLIDE_CAMERA_DROP + elif is_mantling: + target_offset = -0.25 head_offset_y = lerp(head_offset_y, target_offset, 12.0 * delta) - head.position = head_base_pos + bob + Vector3(0, head_offset_y, 0) - # Наклон камеры при стрейфе + # «Проседание» камеры при приземлении + 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 - head.rotation.z = lerp(head.rotation.z, target_roll, 8.0 * delta) + 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: зум > слайд > скорость бега + # 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) diff --git a/main.tscn b/main.tscn index 4e1065e..058f1a8 100644 --- a/main.tscn +++ b/main.tscn @@ -7,8 +7,18 @@ [ext_resource type="PackedScene" uid="uid://db05x4f444mr2" path="res://models/tower_01/Tower_01.glb" id="5_272bh"] [ext_resource type="PackedScene" uid="uid://dr2udwp3pu8g1" path="res://models/house_02/house_02-col.glb" id="6_5vw27"] [ext_resource type="AudioStream" uid="uid://ddsynauqeoppw" path="res://World/tron_01.ogg" id="7_272bh"] +[ext_resource type="AudioStream" uid="uid://280pvcvxt2n2" path="res://models/Bochi-Rock/Guitar.ogg" id="7_dg77c"] [ext_resource type="PackedScene" uid="uid://cqfe5g5ospxic" path="res://models/Tree_01/Tree_01.glb" id="8_5vw27"] [ext_resource type="PackedScene" uid="uid://evml2s8rmerw" path="res://models/obelisk_01/Obelisk.glb" id="9_kek77"] +[ext_resource type="Script" uid="uid://b3ckck8y0rby6" path="res://models/Bochi-Rock/rock-path.gd" id="10_4c57u"] +[ext_resource type="PackedScene" uid="uid://cowp33ejxcfcl" path="res://models/Bochi-Rock/Rock.glb" id="11_efxa6"] + +[sub_resource type="Curve3D" id="Curve3D_dg77c"] +_data = { +"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 5.2467575, -0.0029411316, 93.87327, 0, 0, 0, 0, 0, 0, 13.789459, -0.0029335022, 97.604256, 0, 0, 0, 0, 0, 0, 24.239388, -0.0029335022, 102.18522, 0, 0, 0, 0, 0, 0, 37.65388, -0.0029335022, 101.72713, 0, 0, 0, 0, 0, 0, 49.610317, -0.002937317, 93.816925, 0, 0, 0, 0, 0, 0, 54.721954, -0.002937317, 83.662735, 0, 0, 0, 0, 0, 0, 60.17699, -0.002937317, 67.371086, 0, 0, 0, 0, 0, 0, 63.19221, -0.002937317, 50.906624, 0, 0, 0, 0, 0, 0, 63.427063, -0.002937317, 36.6671, 0, 0, 0, 0, 0, 0, 60.360424, -0.002937317, 23.65346, 0, 0, 0, 0, 0, 0, 51.587524, -0.002937317, 10.339436, 0, 0, 0, 0, 0, 0, 37.92818, -0.002937317, -5.663192, 0, 0, 0, 0, 0, 0, 30.353395, -0.0029411316, -11.97097, 0, 0, 0, 0, 0, 0, 24.506142, -0.002937317, -11.578466, 0, 0, 0, 0, 0, 0, 23.492048, -0.0029411316, -8.094779, 0, 0, 0, 0, 0, 0, 26.842072, -0.0029411316, -2.955284, 0, 0, 0, 0, 0, 0, 30.540104, -0.002937317, -0.6889343, 0, 0, 0, 0, 0, 0, 32.266655, -0.0029411316, 7.519619, 0, 0, 0, 0, 0, 0, 29.88159, -0.002937317, 11.079437, 0, 0, 0, 0, 0, 0, 23.867472, -0.002937317, 15.516203, 0, 0, 0, 0, 0, 0, 20.547432, -0.0029411316, 15.771803, 0, 0, 0, 0, 0, 0, 17.486126, -0.0029411316, 10.689701, 0, 0, 0, 0, 0, 0, 12.567432, -0.0029411316, 9.993235, 0, 0, 0, 0, 0, 0, 8.265545, -0.002937317, 16.372534, 0, 0, 0, 0, 0, 0, 1.9048309, -0.002937317, 26.435099, 0, 0, 0, 0, 0, 0, -9.015602, -0.002937317, 34.162575, 0, 0, 0, 0, 0, 0, -19.431889, -0.002937317, 44.664474, 0, 0, 0, 0, 0, 0, -23.387104, -0.002937317, 59.80117, 0, 0, 0, 0, 0, 0, -21.00288, -0.0029335022, 71.96566, 0, 0, 0, 0, 0, 0, -12.79343, -0.0029335022, 78.17484, 0, 0, 0, 0, 0, 0, -3.5642548, -0.002937317, 81.15604, 0, 0, 0, 0, 0, 0, 5.206661, -0.002937317, 82.6712), +"tilts": PackedFloat32Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +} +point_count = 32 [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_6opy2"] panorama = ExtResource("2_5vw27") @@ -36,14 +46,46 @@ adjustment_enabled = true [node name="Node3D" type="Node3D" unique_id=402787459] -[node name="Floor" parent="." unique_id=1320100733 instance=ExtResource("3_h2yge")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.63628197, -0.002937317, 0.7328758) +[node name="Architecture" type="Node3D" parent="." unique_id=1485328160] metadata/_edit_lock_ = true -[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=726373494] +[node name="building_02" parent="Architecture" unique_id=405025941 instance=ExtResource("4_7mycd")] +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -12.292704, 0, -5.969286) + +[node name="Tower_01" parent="Architecture" unique_id=1436461045 instance=ExtResource("5_272bh")] +transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -12.874907, 9.536743e-07, 21.923906) + +[node name="minecraft-house_02-col" parent="Architecture" unique_id=497625491 instance=ExtResource("6_5vw27")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14.864338, -0.0029354095, 18.549324) + +[node name="Obelisk" parent="Architecture" unique_id=94486605 instance=ExtResource("9_kek77")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 78.88046, -2.6785774, 38.156742) + +[node name="rock" type="Path3D" parent="Architecture" unique_id=1419147983] +curve = SubResource("Curve3D_dg77c") + +[node name="PathFollow3D" type="PathFollow3D" parent="Architecture/rock" unique_id=2095285038] +transform = Transform3D(-0.4002386, -7.500245e-07, -0.916411, 5.684342e-14, 1, -8.184369e-07, 0.916411, -3.275701e-07, -0.4002386, 5.2467575, -0.0029411316, 93.87327) +script = ExtResource("10_4c57u") + +[node name="Rock" parent="Architecture/rock/PathFollow3D" unique_id=1431376827 instance=ExtResource("11_efxa6")] + +[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Architecture/rock/PathFollow3D/Rock" unique_id=912581689] +stream = ExtResource("7_dg77c") +autoplay = true +max_distance = 30.0 + +[node name="Environment" type="Node3D" parent="." unique_id=786334107] +metadata/_edit_lock_ = true + +[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="Environment" unique_id=1278575418] +stream = ExtResource("7_272bh") +autoplay = true + +[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment" unique_id=726373494] environment = SubResource("Environment_x7k75") -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment" unique_id=1584854505] +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment/WorldEnvironment" unique_id=1584854505] transform = Transform3D(-0.72657466, -0.6383954, -0.25404838, 0, -0.36974674, 0.92913264, -0.68708754, 0.6750842, 0.26864862, 0, 3.2902045, 0) light_color = Color(0, 0.24562582, 0.24562573, 1) light_indirect_energy = 0.921 @@ -55,33 +97,23 @@ directional_shadow_split_1 = 0.14 directional_shadow_split_2 = 0.185 directional_shadow_max_distance = 186.7 -[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1278575418] -stream = ExtResource("7_272bh") -autoplay = true +[node name="Floor" parent="Environment" unique_id=1320100733 instance=ExtResource("3_h2yge")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.63628197, -0.002937317, 0.7328758) +metadata/_edit_lock_ = true + +[node name="Nature" type="Node3D" parent="." unique_id=950189180] + +[node name="Tree_01" parent="Nature" unique_id=1867241468 instance=ExtResource("8_5vw27")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.709265, 0.24313244, -13.908665) + +[node name="Tree_01" parent="Nature/Tree_01" unique_id=179570336 instance=ExtResource("8_5vw27")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13.335356, 0.24313053, 59.155888) + +[node name="Tree_01" parent="Nature/Tree_01/Tree_01" unique_id=230889573 instance=ExtResource("8_5vw27")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.854347, 0.2431305, 12.051704) + +[node name="Tree_02" parent="Nature/Tree_01/Tree_01" unique_id=1966462925 instance=ExtResource("8_5vw27")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -43.224022, 0.2431305, -92.65268) [node name="CharacterBody3D" parent="." unique_id=365865796 instance=ExtResource("2_h2yge")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0830812, 2.6370785, 0.44329834) - -[node name="building_02" parent="." unique_id=405025941 instance=ExtResource("4_7mycd")] -transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -12.292704, 0, -5.969286) - -[node name="Tower_01" parent="." unique_id=1436461045 instance=ExtResource("5_272bh")] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -12.874907, 9.536743e-07, 21.923906) - -[node name="house_02-col" parent="." unique_id=497625491 instance=ExtResource("6_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14.864338, -0.0029354095, 18.549324) - -[node name="Tree_01" parent="." unique_id=1867241468 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.709265, 0.24313244, -13.908665) - -[node name="Tree_01" parent="Tree_01" unique_id=179570336 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13.335356, 0.24313053, 59.155888) - -[node name="Tree_01" parent="Tree_01/Tree_01" unique_id=230889573 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.854347, 0.2431305, 12.051704) - -[node name="Tree_02" parent="Tree_01/Tree_01" unique_id=1966462925 instance=ExtResource("8_5vw27")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -43.224022, 0.2431305, -92.65268) - -[node name="Obelisk" parent="." unique_id=94486605 instance=ExtResource("9_kek77")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 78.88046, -2.6785774, 38.156742) diff --git a/models/Bochi-Rock/Guitar.ogg b/models/Bochi-Rock/Guitar.ogg new file mode 100644 index 0000000..0c1b553 Binary files /dev/null and b/models/Bochi-Rock/Guitar.ogg differ diff --git a/models/Bochi-Rock/Guitar.ogg.import b/models/Bochi-Rock/Guitar.ogg.import new file mode 100644 index 0000000..d5f424d --- /dev/null +++ b/models/Bochi-Rock/Guitar.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://280pvcvxt2n2" +path="res://.godot/imported/Guitar.ogg-21bf9bad0fcf3bfe7341e2b1bd9308b9.oggvorbisstr" + +[deps] + +source_file="res://models/Bochi-Rock/Guitar.ogg" +dest_files=["res://.godot/imported/Guitar.ogg-21bf9bad0fcf3bfe7341e2b1bd9308b9.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/models/Bochi-Rock/Rock.glb b/models/Bochi-Rock/Rock.glb new file mode 100644 index 0000000..0d7a66c Binary files /dev/null and b/models/Bochi-Rock/Rock.glb differ diff --git a/models/Bochi-Rock/Rock.glb.import b/models/Bochi-Rock/Rock.glb.import new file mode 100644 index 0000000..4552827 --- /dev/null +++ b/models/Bochi-Rock/Rock.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cowp33ejxcfcl" +path="res://.godot/imported/Rock.glb-4f2376a14ce37861c14e06a47e678e34.scn" + +[deps] + +source_file="res://models/Bochi-Rock/Rock.glb" +dest_files=["res://.godot/imported/Rock.glb-4f2376a14ce37861c14e06a47e678e34.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg b/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg new file mode 100644 index 0000000..7347c35 Binary files /dev/null and b/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg differ diff --git a/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg.import b/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg.import new file mode 100644 index 0000000..d268ef9 --- /dev/null +++ b/models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg.import @@ -0,0 +1,44 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bx6t5621iqw7f" +path.s3tc="res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-fad3b4615e62f76758b5970cba2671a7.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} +generator_parameters={ +"md5": "d87e4757e8f4306abfb2713939bf67fa" +} + +[deps] + +source_file="res://models/Bochi-Rock/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg" +dest_files=["res://.godot/imported/Rock_22ee8cdbd4ccc631cd7c804ab4ebca15.jpg-fad3b4615e62f76758b5970cba2671a7.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/models/Bochi-Rock/rock-path.gd b/models/Bochi-Rock/rock-path.gd new file mode 100644 index 0000000..55cbe91 --- /dev/null +++ b/models/Bochi-Rock/rock-path.gd @@ -0,0 +1,6 @@ +extends PathFollow3D + +@export var speed := 7.0 # единиц пути в секунду + +func _process(delta): + progress += speed * delta diff --git a/models/Bochi-Rock/rock-path.gd.uid b/models/Bochi-Rock/rock-path.gd.uid new file mode 100644 index 0000000..9ab8fe7 --- /dev/null +++ b/models/Bochi-Rock/rock-path.gd.uid @@ -0,0 +1 @@ +uid://b3ckck8y0rby6