extends CharacterBody2D # SoundManager resolved from the autoload tree at runtime func _get_sound() -> Node: return get_tree().root.get_node_or_null("SoundManager") # === НАСТРОЙКИ ФИЗИКИ === const GRAVITY: float = 980.0 const WALK_SPEED: float = 200.0 const RUN_SPEED: float = 350.0 const JUMP_VELOCITY: float = -450.0 # === ПРОДВИНУТЫЕ МЕХАНИКИ === const COYOTE_TIME: float = 0.1 # Секунды после схода с платформы, когда ещё можно прыгнуть const JUMP_BUFFER: float = 0.15 # Секунды, когда нажатие прыжка "запоминается" до приземления const JUMP_CUT_MULT: float = 0.4 # Обрезка скорости подъёма при отпускании кнопки прыжка # Высота, ниже которой лиса считается упавшей в пропасть const DEATH_Y: float = 900.0 var coyote_timer: float = 0.0 var jump_buffer_timer: float = 0.0 var is_running: bool = false var jump_cut_done: bool = true # true = обрезка уже применена (или прыжка нет) # Направление взгляда (для будущего спрайта) var facing_right: bool = true # Точка возрождения (задаётся уровнем) var spawn_point: Vector2 = Vector2.ZERO # Защита от повторной обработки смерти в одном кадре var is_dying: bool = false func _ready() -> void: spawn_point = position func _physics_process(delta: float) -> void: if is_dying: return # 1. Гравитация + Coyote Time # ВАЖНО: maxf (не maxi!) — таймеры дробные, maxi обнулял бы их каждый кадр. if not is_on_floor(): velocity.y += GRAVITY * delta coyote_timer = maxf(coyote_timer - delta, 0.0) else: coyote_timer = COYOTE_TIME # Сброс при касании земли # 2. Jump Buffer if Input.is_action_just_pressed("jump"): jump_buffer_timer = JUMP_BUFFER else: jump_buffer_timer = maxf(jump_buffer_timer - delta, 0.0) # 3. Выполнение прыжка (буфер + coyote) if jump_buffer_timer > 0.0 and coyote_timer > 0.0: velocity.y = JUMP_VELOCITY jump_buffer_timer = 0.0 coyote_timer = 0.0 # запрет повторного прыжка в воздухе jump_cut_done = false # TODO: Звук прыжка + эмиссия частиц # 4. Variable Jump Height — обрезаем ОДИН раз при отпускании кнопки if not jump_cut_done and velocity.y < 0.0 and not Input.is_action_pressed("jump"): velocity.y *= JUMP_CUT_MULT jump_cut_done = true # 5. Горизонтальное движение var direction: float = Input.get_axis("move_left", "move_right") is_running = Input.is_action_pressed("run") var current_speed: float = RUN_SPEED if is_running else WALK_SPEED if direction != 0.0: velocity.x = direction * current_speed facing_right = direction > 0.0 # TODO: сменить анимацию на 'run' else: velocity.x = move_toward(velocity.x, 0.0, current_speed) # TODO: сменить анимацию на 'idle' # 6. Движение и коллизии var was_rising: bool = velocity.y < 0.0 move_and_slide() # 6.1 Удар головой снизу — сообщаем блоку if was_rising: for i in range(get_slide_collision_count()): var col := get_slide_collision(i) # normal указывает ОТ поверхности к телу; при ударе снизу normal.y > 0 if col.get_normal().y > 0.5: var other := col.get_collider() if other != null and other.has_method("on_hit_from_below"): other.on_hit_from_below() # 7. Падение в пропасть if position.y > DEATH_Y: die() func hit_coin(points: int) -> void: GameManager.add_score(points) var sfx := _get_sound() if sfx != null and sfx.has_method("play"): sfx.call("play", "coin") func die() -> void: if is_dying: return is_dying = true velocity = Vector2.ZERO var sfx := _get_sound() if sfx != null and sfx.has_method("play"): sfx.call("play", "die") if GameManager.lose_life(): # Возрождение GameManager.respawn() position = spawn_point velocity = Vector2.ZERO is_dying = false else: # Жизни кончились get_tree().quit() # TODO: переключение анимаций (idle / run / jump / fall)