class_name EnemyBase extends CharacterBody2D @export var move_speed: float = 100 @export var max_health: float = 10.0 @export var default_contact_damage: float = 0.0 @export var target_distance: float = 6.0 @export var path_update_interval: float = 1.5 @export var xp_dropped: float = 5.0 @export var enemy_rarity: GlobalConst.Rarity = GlobalConst.Rarity.NORMAL var modifiers: Array[EnemyMod] = [] @onready var target_cast: RayCast2D = $TargetCast @onready var animation_player: AnimationPlayer = $AnimationPlayer @onready var contact_damage_cd: Timer = $ContactDamageCD @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D @onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D @onready var shape_cast_2d: ShapeCast2D = $ShapeCast2D @onready var sprite_2d: Sprite2D = $Sprite2D @onready var label: Label = $HBoxContainer/Label @onready var effect_container: HBoxContainer = $HBoxContainer/EffectContainer var player: Player var enemy_name: String var target: Node2D var god_mode: bool = false var is_dead: bool = false var health: float var effects: Array[EnemyEffectBase] var _path_update_timer: float = 0.0 var _compute_cache: KeyedCache = KeyedCache.new() var _effects_visible = [] func _ready() -> void: enemy_name = _gen_name() match enemy_rarity: GlobalConst.Rarity.NORMAL: label.visible = false GlobalConst.Rarity.RARE: var mods = EnemyModPool.get_random_mods(2) label.visible = true modifiers = mods GlobalConst.Rarity.EPIC: var mods = EnemyModPool.get_random_mods(4) label.visible = true modifiers = mods if modifiers.size() > 0: enemy_name += " the %s" % modifiers.pick_random().adjective label.add_theme_color_override("font_color", GlobalConst.rarity_to_color(enemy_rarity)) label.text = enemy_name health = get_calculated("max_health") shape_cast_2d.shape.radius = collision_shape_2d.shape.radius shape_cast_2d.enabled = false sprite_2d.material = sprite_2d.material.duplicate() _find_player() func _find_player(): if not player: player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER) target = player func _gen_name() -> String: return "Unnamed enemy" func _process(delta: float) -> void: for effect in effects: if effect in _effects_visible: continue var effect_sprite = Sprite2D.new() effect_sprite.texture = preload("res://assets/sprites/small_bleed_icon.png") effect_container.add_child(effect_sprite) _effects_visible.append(effect) func _physics_process(delta: float) -> void: if not target: return do_movement(delta) check_contact_damage() func do_movement(delta: float) -> void: if not target: return if global_position.distance_to(target.global_position) < target_distance: return _path_update_timer -= delta + randf() if _has_direct_path(): shape_cast_2d.enabled = false _do_simple_movement() else: _do_nav_agent_movement() if velocity.x < 0: sprite_2d.flip_h = true else: sprite_2d.flip_h = false func _has_direct_path(): target_cast.target_position = to_local(target.global_position) target_cast.enabled = true return not target_cast.is_colliding() func _do_simple_movement(): var direction = global_position.direction_to(target.global_position) var distance = global_position.distance_to(target.global_position) if distance > 4: velocity = direction * get_calculated("move_speed") move_and_slide() func _do_nav_agent_movement(): if _path_update_timer <= 0.0: _path_update_timer = path_update_interval nav_agent.target_position = target.global_position if nav_agent.is_navigation_finished(): return var next_point = nav_agent.get_next_path_position() var direction = (next_point - global_position).normalized() shape_cast_2d.target_position = to_local(next_point) shape_cast_2d.enabled = true if shape_cast_2d.is_colliding(): direction = direction.bounce(shape_cast_2d.get_collision_normal(0)).normalized() velocity = direction * get_calculated("move_speed") move_and_slide() func check_contact_damage(): if default_contact_damage == 0.0: return if global_position.distance_to(target.global_position) > target_distance + 2: return deal_contact_damage() func deal_contact_damage(): if target.is_in_group("damagable"): if contact_damage_cd.is_stopped(): target.take_damage(default_contact_damage) contact_damage_cd.start() func take_damage(value: float, is_crit: bool = false): if god_mode: return health -= value var dm = preload("res://scenes/damage_numbers.tscn").instantiate() dm.damage_taken = value dm.critical_damage = is_crit animation_player.play("generic_anims/take_damage") add_child(dm) if health <= 0: die() func cheer(): target_distance = 2 if not animation_player.is_playing(): animation_player.play("generic_anims/cheer") func die(): if is_dead: return is_dead = true drop_xp_orb() target = null velocity = Vector2.ZERO animation_player.play("generic_anims/die") func drop_xp_orb() -> void: var orb: XPOrb = preload("res://scenes/xp_orb.tscn").instantiate() orb.value = xp_dropped * (1 + (modifiers.size() * 3)) orb.position = position get_parent().call_deferred("add_child", orb) func _on_animation_player_animation_finished(anim_name: StringName) -> void: if is_dead: queue_free() func get_calculated(key: String) -> Variant: # set max move speed to players move speed var compute_func = func(): if key == "move_speed": return clampf( EnemyMod.get_calculated(self, key), 0, player.player_stats.get_final("move_speed", player.modifiers) ) return EnemyMod.get_calculated(self, key) return _compute_cache.get_or_compute(key, compute_func) func has_property(key: String) -> bool: var cache_key = "prop_%s" % key var compute_func = func(): return get(key) != null return _compute_cache.get_or_compute(key, compute_func)