extends CharacterBody2D # ============================================================================ # MOVIMIENTO # ============================================================================ @export_category("Movement") @export var speed: float = 300.0 @export var ground_acceleration: float = 2200.0 @export var ground_friction: float = 2600.0 @export var air_acceleration: float = 1000.0 # ============================================================================ # SALTO # ============================================================================ @export_category("Jump") @export var jump_velocity: float = -600.0 # Permite saltar ligeramente después de abandonar el suelo. @export var coyote_time: float = 0.12 # Permite pulsar salto ligeramente antes de tocar el suelo. @export var jump_buffer_time: float = 0.12 # Al soltar el botón durante la subida reducimos la velocidad vertical. # Cuanto más pequeño, más brusco será el salto corto. @export_range(0.1, 1.0) var jump_cut_multiplier: float = 0.45 # La gravedad normal durante la subida. @export var gravity_multiplier: float = 1.0 # La caída es más rápida que la subida. @export var fall_gravity_multiplier: float = 1.7 @export var max_fall_speed: float = 1200.0 # ============================================================================ # PUÑETAZO # ============================================================================ @export_category("Punch") @export var punch_damage: int = 1 @export var punch_duration: float = 0.13 @export var punch_cooldown: float = 0.28 @export var punch_hitbox_size := Vector2(55.0, 38.0) @export var punch_offset := Vector2(38.0, 0.0) # ============================================================================ # GROUND POUND # ============================================================================ @export_category("Ground Pound") @export var ground_pound_damage: int = 3 # Velocidad mínima que adquiere al comenzar el golpe hacia abajo. @export var ground_pound_start_speed: float = 450.0 # Gravedad adicional mientras cae haciendo ground pound. @export var ground_pound_gravity_multiplier: float = 4.5 # Control horizontal mientras cae. @export_range(0.0, 1.0) var ground_pound_horizontal_control: float = 0.25 @export var ground_pound_radius: float = 85.0 @export var ground_pound_offset := Vector2(0.0, 20.0) # ============================================================================ # DEBUG # ============================================================================ @export_category("Debug") @export var show_attack_hitboxes: bool = true @export var debug_impact_duration: float = 0.18 # ============================================================================ # NODOS # ============================================================================ @onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D @onready var punch_hitbox: Area2D = $PunchHitbox @onready var punch_collision: CollisionShape2D = $PunchHitbox/CollisionShape2D @onready var ground_pound_hitbox: Area2D = $GroundPoundHitbox @onready var ground_pound_collision: CollisionShape2D = \ $GroundPoundHitbox/CollisionShape2D # ============================================================================ # ESTADO INTERNO # ============================================================================ var facing_direction: float = 1.0 var coyote_timer: float = 0.0 var jump_buffer_timer: float = 0.0 var punch_timer: float = 0.0 var attack_cooldown_timer: float = 0.0 var ground_pound_active: bool = false var ground_pound_debug_timer: float = 0.0 # Evita que un enemigo reciba daño 60 veces durante el mismo puñetazo. var punch_hit_targets: Dictionary = {} func _ready() -> void: _setup_hitboxes() func _physics_process(delta: float) -> void: var was_on_floor := is_on_floor() _update_timers(delta) _update_jump_windows(delta) _handle_attack_input() _handle_jump() _apply_gravity(delta) _handle_horizontal_movement(delta) _update_punch_damage() _update_animation() move_and_slide() # El impacto debe comprobarse DESPUÉS de move_and_slide(), # porque es entonces cuando sabemos si hemos aterrizado. if ground_pound_active and not was_on_floor and is_on_floor(): _ground_pound_impact() queue_redraw() # ============================================================================ # MOVIMIENTO # ============================================================================ func _handle_horizontal_movement(delta: float) -> void: var direction := Input.get_axis("move_left", "move_right") if direction != 0.0: facing_direction = sign(direction) _update_facing() if ground_pound_active: var target_speed := ( direction * speed * ground_pound_horizontal_control ) velocity.x = move_toward( velocity.x, target_speed, air_acceleration * delta ) return if is_on_floor(): if direction != 0.0: velocity.x = move_toward( velocity.x, direction * speed, ground_acceleration * delta ) else: velocity.x = move_toward( velocity.x, 0.0, ground_friction * delta ) else: velocity.x = move_toward( velocity.x, direction * speed, air_acceleration * delta ) func _update_facing() -> void: animated_sprite.flip_h = facing_direction < 0.0 punch_hitbox.position = Vector2( abs(punch_offset.x) * facing_direction, punch_offset.y ) # ============================================================================ # GRAVEDAD # ============================================================================ func _apply_gravity(delta: float) -> void: if is_on_floor(): return var gravity := get_gravity() if ground_pound_active: velocity += ( gravity * ground_pound_gravity_multiplier * delta ) elif velocity.y > 0.0: # Más gravedad al caer. velocity += gravity * fall_gravity_multiplier * delta else: # Gravedad normal durante la subida. velocity += gravity * gravity_multiplier * delta velocity.y = min(velocity.y, max_fall_speed) # ============================================================================ # SALTO # ============================================================================ func _update_jump_windows(delta: float) -> void: if is_on_floor(): coyote_timer = coyote_time else: coyote_timer = max(coyote_timer - delta, 0.0) if Input.is_action_just_pressed("jump"): jump_buffer_timer = jump_buffer_time else: jump_buffer_timer = max( jump_buffer_timer - delta, 0.0 ) func _handle_jump() -> void: if ( jump_buffer_timer > 0.0 and coyote_timer > 0.0 and not ground_pound_active ): velocity.y = jump_velocity jump_buffer_timer = 0.0 coyote_timer = 0.0 # Salto variable. # # Mantienes pulsado: # salto alto # # Sueltas pronto: # salto corto if ( Input.is_action_just_released("jump") and velocity.y < 0.0 ): velocity.y *= jump_cut_multiplier # ============================================================================ # ATAQUE # ============================================================================ func _handle_attack_input() -> void: if not Input.is_action_just_pressed("attack"): return if attack_cooldown_timer > 0.0: return if is_on_floor(): _start_punch() else: _start_ground_pound() # ============================================================================ # PUÑETAZO # ============================================================================ func _start_punch() -> void: punch_timer = punch_duration attack_cooldown_timer = punch_cooldown punch_hit_targets.clear() func _update_punch_damage() -> void: if punch_timer <= 0.0: return for body in punch_hitbox.get_overlapping_bodies(): _damage_target( body, punch_damage, punch_hit_targets ) for area in punch_hitbox.get_overlapping_areas(): _damage_target( area, punch_damage, punch_hit_targets ) # ============================================================================ # GROUND POUND # ============================================================================ func _start_ground_pound() -> void: if ground_pound_active: return ground_pound_active = true attack_cooldown_timer = punch_cooldown # Si todavía estamos subiendo, cortamos inmediatamente la subida. velocity.y = max( velocity.y, ground_pound_start_speed ) func _ground_pound_impact() -> void: ground_pound_active = false ground_pound_debug_timer = debug_impact_duration var hit_targets: Dictionary = {} for body in ground_pound_hitbox.get_overlapping_bodies(): _damage_target( body, ground_pound_damage, hit_targets ) for area in ground_pound_hitbox.get_overlapping_areas(): _damage_target( area, ground_pound_damage, hit_targets ) # ============================================================================ # DAÑO # ============================================================================ func _damage_target( collider: Node, damage: int, hit_targets: Dictionary ) -> void: if collider == self: return var target := collider # Esto permite que el enemigo tenga una Hurtbox Area2D. if not target.has_method("take_damage"): var parent := target.get_parent() if parent != null and parent.has_method("take_damage"): target = parent if not target.has_method("take_damage"): return var target_id := target.get_instance_id() if hit_targets.has(target_id): return hit_targets[target_id] = true target.take_damage( damage, global_position ) # ============================================================================ # TIMERS # ============================================================================ func _update_timers(delta: float) -> void: punch_timer = max( punch_timer - delta, 0.0 ) attack_cooldown_timer = max( attack_cooldown_timer - delta, 0.0 ) ground_pound_debug_timer = max( ground_pound_debug_timer - delta, 0.0 ) # ============================================================================ # ANIMACIONES # ============================================================================ func _update_animation() -> void: if ground_pound_active: animated_sprite.play("walk") elif not is_on_floor(): animated_sprite.play("walk") elif abs(velocity.x) > 10.0: animated_sprite.play("walk") else: animated_sprite.play("idle") # ============================================================================ # CONFIGURACIÓN DE HITBOX # ============================================================================ func _setup_hitboxes() -> void: # Puñetazo var punch_shape := RectangleShape2D.new() punch_shape.size = punch_hitbox_size punch_collision.shape = punch_shape punch_hitbox.collision_layer = 0 punch_hitbox.collision_mask = 2 punch_hitbox.monitoring = true # Ground pound var pound_shape := CircleShape2D.new() pound_shape.radius = ground_pound_radius ground_pound_collision.shape = pound_shape ground_pound_hitbox.position = ground_pound_offset ground_pound_hitbox.collision_layer = 0 ground_pound_hitbox.collision_mask = 2 ground_pound_hitbox.monitoring = true _update_facing() # ============================================================================ # DEBUG HITBOXES # ============================================================================ func _draw() -> void: if not show_attack_hitboxes: return # Puñetazo if punch_timer > 0.0: var center := punch_hitbox.position var rect := Rect2( center - punch_hitbox_size / 2.0, punch_hitbox_size ) draw_rect( rect, Color(1.0, 0.1, 0.1, 0.25), true ) draw_rect( rect, Color(1.0, 0.1, 0.1, 0.9), false, 2.0 ) # Ground pound: # solamente mostramos el área cuando realmente produce daño. if ground_pound_debug_timer > 0.0: var center := ground_pound_hitbox.position draw_circle( center, ground_pound_radius, Color(1.0, 0.5, 0.0, 0.20) ) draw_arc( center, ground_pound_radius, 0.0, TAU, 48, Color(1.0, 0.5, 0.0, 0.9), 3.0 )