extends CharacterBody2D signal died # First real enemy on purpose: no generic Enemy base class yet. # Once a second/third enemy exists, we can extract only the behaviour they # actually share instead of guessing an abstraction in advance. const PLAYER_HURTBOX_MASK: int = 1 << 3 @export_category("Movement") @export var move_speed: float = 85.0 @export var max_fall_speed: float = 1200.0 @export_category("Combat") @export_range(1, 100, 1) var max_health: int = 3 @export_range(0, 100, 1) var contact_damage: int = 1 @export var contact_damage_interval: float = 0.45 @export_category("Debug") ## Enabled while the enemy has no final sprite/animation. The grey rectangle is ## the temporary body; red overlays are the gameplay hurtbox/contact hitbox. @export var show_debug_hitboxes: bool = true @onready var body_collision: CollisionShape2D = $CollisionShape2D @onready var hurtbox_collision: CollisionShape2D = $Hurtbox/CollisionShape2D @onready var contact_hitbox: Area2D = $ContactHitbox @onready var contact_collision: CollisionShape2D = $ContactHitbox/CollisionShape2D @onready var floor_probe: RayCast2D = $FloorProbe var current_health: int var move_direction: float = -1.0 var contact_damage_timer: float = 0.0 var is_dead: bool = false func _ready() -> void: current_health = max_health contact_hitbox.collision_layer = 0 contact_hitbox.collision_mask = PLAYER_HURTBOX_MASK contact_hitbox.monitoring = true _update_floor_probe() queue_redraw() func _physics_process(delta: float) -> void: if is_dead: return contact_damage_timer = max(contact_damage_timer - delta, 0.0) _apply_gravity(delta) _turn_before_ledge() velocity.x = move_direction * move_speed move_and_slide() if is_on_wall(): _turn_around() _damage_player_on_contact() queue_redraw() func _apply_gravity(delta: float) -> void: if is_on_floor(): return velocity += get_gravity() * delta velocity.y = min(velocity.y, max_fall_speed) func _turn_before_ledge() -> void: # The ray sits just in front of the feet. If it stops finding floor while the # enemy is grounded, turning here prevents the basic walker from suiciding # off every platform. if is_on_floor() and not floor_probe.is_colliding(): _turn_around() func _turn_around() -> void: move_direction *= -1.0 _update_floor_probe() func _update_floor_probe() -> void: floor_probe.position.x = abs(floor_probe.position.x) * move_direction func _damage_player_on_contact() -> void: if contact_damage_timer > 0.0: return for area in contact_hitbox.get_overlapping_areas(): var target: Node = area.get_parent() if target != null and target.has_method("take_damage"): target.take_damage(contact_damage, global_position) contact_damage_timer = contact_damage_interval return func take_damage(amount: int, source_position: Vector2) -> void: if amount <= 0 or is_dead: return current_health = max(current_health - amount, 0) if current_health == 0: _die() return # Turn away from the attacker. This gives a tiny readable reaction without # inventing a hit-stun/knockback system before it is actually needed. var reaction_direction: float = signf(global_position.x - source_position.x) if reaction_direction != 0.0: move_direction = reaction_direction _update_floor_probe() func _die() -> void: is_dead = true died.emit() queue_free() # ============================================================================ # TEMPORARY VISUAL + DEBUG HITBOXES # ============================================================================ func _draw() -> void: # The enemy has no sprite yet. This neutral block makes the actual entity # visible independently from the red debug overlays. _draw_rectangle_collision( body_collision, Color(0.18, 0.18, 0.20, 0.90), Color(0.85, 0.85, 0.90, 1.0) ) if not show_debug_hitboxes: return # Hurtbox: where punches / ground pound are allowed to damage the enemy. _draw_rectangle_collision( hurtbox_collision, Color(1.0, 0.05, 0.05, 0.12), Color(1.0, 0.05, 0.05, 0.55) ) # Contact hitbox: slightly larger, so it is obvious exactly when touching # the enemy can damage the player. _draw_rectangle_collision( contact_collision, Color(1.0, 0.05, 0.05, 0.18), Color(1.0, 0.05, 0.05, 0.85), contact_hitbox.position ) func _draw_rectangle_collision( collision: CollisionShape2D, fill_color: Color, outline_color: Color, extra_offset: Vector2 = Vector2.ZERO ) -> void: var rectangle: RectangleShape2D = collision.shape as RectangleShape2D if rectangle == null: return var center: Vector2 = collision.position + extra_offset var rect: Rect2 = Rect2(center - rectangle.size / 2.0, rectangle.size) draw_rect(rect, fill_color, true) draw_rect(rect, outline_color, false, 2.0)