extends BaseEnemy class_name BasicEnemy ## A basic melee enemy that chases and attacks players ## Server-authoritative AI with multiplayer support ## Movement @export var move_speed: float = 3.0 @export var chase_range: float = 15.0 @export var attack_range: float = 2.5 ## Combat @export var attack_damage: float = 15.0 @export var attack_knockback: float = 10.0 @export var attack_cooldown: float = 1.5 @export_category("Attack Timing") @export var attack_startup: float = 0.3 # Wind-up before hit @export var attack_active: float = 0.4 # Hit window duration ## References var _hitbox: HitBox = null var _hurtbox: HurtBox = null var _mesh: MeshInstance3D = null ## AI State var _attack_timer: float = 0.0 var _is_attacking: bool = false var _original_material: Material = null var _hit_flash_timer: float = 0.0 const HIT_FLASH_DURATION: float = 0.2 func _enter_tree(): # Enemies are always server-authoritative set_multiplayer_authority(1) func _ready(): super._ready() # Find mesh, hitbox, and hurtbox _mesh = get_node_or_null("Mesh") _hitbox = get_node_or_null("HitBox") _hurtbox = get_node_or_null("HurtBox") # Store original material for hit flash if _mesh: _original_material = _mesh.get_surface_override_material(0) if not _original_material and _mesh.mesh: _original_material = _mesh.mesh.surface_get_material(0) # Setup hitbox if _hitbox: _hitbox.owner_entity = self _hitbox.set_stats(attack_damage, attack_knockback) _hitbox.hit_landed.connect(_on_hitbox_hit) # Setup hurtbox if _hurtbox: _hurtbox.owner_entity = self func _process(delta): # Countdown timers if _attack_timer > 0: _attack_timer -= delta # Handle hit flash if _hit_flash_timer > 0: _hit_flash_timer -= delta if _hit_flash_timer <= 0: _reset_material() func _physics_process(delta): super._physics_process(delta) # Only server runs AI if not multiplayer.is_server(): return if is_dead: return # Apply gravity if not is_on_floor(): velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta # AI behavior if current_target and is_instance_valid(current_target): _ai_combat(delta) else: # Stop moving if no target velocity.x = 0 velocity.z = 0 move_and_slide() ## Override to find nearest player without range limit func get_nearest_player() -> Node: # Find all players in a very large range (essentially unlimited) var players = get_players_in_range(1000.0) # 1000m range - basically unlimited if players.is_empty(): return null var nearest_player = null var nearest_distance = INF for player in players: var distance = global_position.distance_to(player.global_position) if distance < nearest_distance: nearest_distance = distance nearest_player = player return nearest_player ## Update target - called by BaseEnemy func _update_target(): if not is_aggressive: return # Always find and target the nearest player (no range limit) var nearest = get_nearest_player() if nearest: # Update target if it changed if current_target != nearest: current_target = nearest target_changed.emit(nearest) else: # No players exist if current_target != null: current_target = null target_changed.emit(null) ## Combat AI behavior func _ai_combat(delta): if not current_target or not is_instance_valid(current_target): return var target_pos = current_target.global_position var direction = (target_pos - global_position).normalized() var distance = global_position.distance_to(target_pos) # Face the target if direction.length() > 0.01: var look_dir = Vector3(direction.x, 0, direction.z) if look_dir.length() > 0.01: look_at(global_position + look_dir, Vector3.UP) # If in attack range, attack if distance <= attack_range: # Stop moving when attacking velocity.x = 0 velocity.z = 0 # Try to attack if _attack_timer <= 0 and not _is_attacking: _perform_attack() else: # Always chase the target (no range limit) velocity.x = direction.x * move_speed velocity.z = direction.z * move_speed ## Perform melee attack func _perform_attack(): if _is_attacking or not multiplayer.is_server(): return # Calculate total attack duration var total_duration = attack_startup + attack_active var cooldown = max(attack_cooldown, total_duration) _attack_timer = cooldown _is_attacking = true # Play attack animation on all clients rpc("_sync_attack_animation") # Activate hitbox _activate_hitbox() ## Activate hitbox for attack func _activate_hitbox(): if not _hitbox or not multiplayer.is_server(): _is_attacking = false return # STARTUP PHASE - Wait before activating if attack_startup > 0: await get_tree().create_timer(attack_startup).timeout if not _hitbox or not is_instance_valid(_hitbox) or is_dead: _is_attacking = false return # ACTIVE PHASE - Hitbox on _hitbox.activate() await get_tree().create_timer(attack_active).timeout # RECOVERY PHASE - Hitbox off if _hitbox and is_instance_valid(_hitbox): _hitbox.deactivate() _is_attacking = false ## Called when hitbox hits something func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): if not target or not multiplayer.is_server(): return # Flash target's hurtbox if target is Node: var hurtbox = target.find_child("HurtBox", true, false) if hurtbox and hurtbox.has_method("flash_hit"): hurtbox.flash_hit() # Server applies damage directly if target is BaseUnit: target.take_damage(damage_amount, 1, knockback_amount, global_position) ## Play attack animation (synced to all clients) @rpc("any_peer", "call_local", "reliable") func _sync_attack_animation(): # Visual feedback for attack # Could play animation here if you have an AnimationPlayer pass ## Override hurt animation to flash red @rpc("any_peer", "call_local", "reliable") func _play_hurt_animation(): if _mesh: _flash_red() ## Flash red when hit func _flash_red(): if not _mesh: return _hit_flash_timer = HIT_FLASH_DURATION # Create red material var red_material = StandardMaterial3D.new() red_material.albedo_color = Color(1.5, 0.3, 0.3) # Copy properties from original if exists if _original_material and _original_material is StandardMaterial3D: var orig = _original_material as StandardMaterial3D red_material.metallic = orig.metallic red_material.roughness = orig.roughness red_material.albedo_texture = orig.albedo_texture _mesh.set_surface_override_material(0, red_material) ## Reset material func _reset_material(): if _mesh and _original_material: _mesh.set_surface_override_material(0, _original_material.duplicate()) ## Death callback func _on_enemy_died(killer_id: int): super._on_enemy_died(killer_id) # Hide when dead if _mesh: _mesh.visible = false # Deactivate hitbox if _hitbox: _hitbox.deactivate() print("[BasicEnemy ", name, "] killed by ", killer_id) ## Respawn callback func _on_enemy_respawned(): super._on_enemy_respawned() # Show mesh if _mesh: _mesh.visible = true _reset_material() # Reset state _attack_timer = 0.0 _is_attacking = false print("[BasicEnemy ", name, "] respawned")