116 lines
3.8 KiB
GDScript
116 lines
3.8 KiB
GDScript
extends CharacterBody2D
|
|
class_name EnemyBase
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stats — override in derived enemy types
|
|
# ---------------------------------------------------------------------------
|
|
var max_health: float = 10.0
|
|
var current_health: float = 10.0
|
|
var speed: float = 60.0
|
|
var contact_damage: float = 1.0
|
|
var contact_cooldown: float = 0.8
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal state — shared across all enemies
|
|
# ---------------------------------------------------------------------------
|
|
const SPAWN_DELAY: float = 0.5
|
|
|
|
var _contact_timer: float = 0.0
|
|
var _flash_timer: float = 0.0
|
|
var _slow_effects: Array[Dictionary] = [] # [{factor, timer}] - stacking slows
|
|
var _spawn_timer: float = SPAWN_DELAY
|
|
var _player: Node2D = null
|
|
|
|
@onready var visual: Node2D = $Visual
|
|
|
|
func _ready() -> void:
|
|
add_to_group("enemies")
|
|
$ContactArea.body_entered.connect(_on_contact_entered)
|
|
|
|
func is_active() -> bool:
|
|
return _spawn_timer <= 0.0
|
|
|
|
func _tick_spawn_delay(delta: float) -> void:
|
|
if _spawn_timer > 0.0:
|
|
_spawn_timer -= delta
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared helpers — call from derived _physics_process as needed
|
|
# ---------------------------------------------------------------------------
|
|
func _find_player() -> void:
|
|
if _player == null or not is_instance_valid(_player):
|
|
_player = get_tree().get_first_node_in_group("player")
|
|
|
|
func _tick_contact(delta: float) -> void:
|
|
if _contact_timer > 0.0:
|
|
_contact_timer -= delta
|
|
if _contact_timer <= 0.0:
|
|
_contact_timer = 0.0
|
|
# Re-apply damage if player is still overlapping after cooldown
|
|
for body in $ContactArea.get_overlapping_bodies():
|
|
if body.is_in_group("player"):
|
|
body.take_damage(contact_damage)
|
|
_contact_timer = contact_cooldown
|
|
break
|
|
|
|
func _tick_flash(delta: float) -> void:
|
|
if _flash_timer > 0.0:
|
|
_flash_timer -= delta
|
|
visual.modulate = Color(2.5, 2.5, 2.5)
|
|
else:
|
|
visual.modulate = Color(1.0, 1.0, 1.0)
|
|
|
|
func apply_slow(duration: float, factor: float) -> void:
|
|
# Stack slows by adding new effect - factors multiply together
|
|
_slow_effects.append({"factor": factor, "timer": duration})
|
|
|
|
func clear_slows() -> void:
|
|
_slow_effects.clear()
|
|
|
|
func _tick_slow(delta: float) -> void:
|
|
# Tick down all slow effects and remove expired ones
|
|
var i := _slow_effects.size() - 1
|
|
while i >= 0:
|
|
_slow_effects[i]["timer"] -= delta
|
|
if _slow_effects[i]["timer"] <= 0.0:
|
|
_slow_effects.remove_at(i)
|
|
i -= 1
|
|
|
|
func get_slow_factor() -> float:
|
|
# Multiply all active slow factors together
|
|
var combined := 1.0
|
|
for effect in _slow_effects:
|
|
combined *= effect["factor"]
|
|
return combined
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Damage / death
|
|
# ---------------------------------------------------------------------------
|
|
func take_damage(amount: float) -> void:
|
|
current_health -= amount
|
|
_flash_timer = 0.1
|
|
AudioManager.play("enemy_hit")
|
|
if current_health <= 0.0:
|
|
_die()
|
|
|
|
func _die() -> void:
|
|
AudioManager.play("enemy_death")
|
|
_spawn_drop()
|
|
queue_free()
|
|
|
|
func _spawn_drop() -> void:
|
|
if not RunManager.run_active:
|
|
return # No drops in practice/hub
|
|
var orb = preload("res://scenes/enemies/currency_orb.tscn").instantiate()
|
|
orb.position = (get_parent() as Node2D).to_local(global_position)
|
|
get_parent().call_deferred("add_child", orb)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Contact damage
|
|
# ---------------------------------------------------------------------------
|
|
func _on_contact_entered(body: Node) -> void:
|
|
if not is_active():
|
|
return
|
|
if body.is_in_group("player") and _contact_timer == 0.0:
|
|
body.take_damage(contact_damage)
|
|
_contact_timer = contact_cooldown |