177 lines
4.7 KiB
GDScript
177 lines
4.7 KiB
GDScript
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)
|