extends "res://scripts/enemy_base.gd"

# Humanoid Enemy - Uses same sprite structure as player with multiple layers

enum HumanoidType {
	CYCLOPS,
	DEMON,
	HUMANOID,
	NIGHTELF,
	GOBLIN,
	ORC,
	SKELETON
}

@export var humanoid_type: HumanoidType = HumanoidType.HUMANOID
@export var randomize_appearance_on_respawn: bool = false

# Character sprite layers (same as player)
@onready var sprite_body = $Sprite2DBody
@onready var sprite_boots = $Sprite2DBoots
@onready var sprite_armour = $Sprite2DArmour
@onready var sprite_facial_hair = $Sprite2DFacialHair
@onready var sprite_hair = $Sprite2DHair
@onready var sprite_eyes = $Sprite2DEyes
@onready var sprite_eyelashes = $Sprite2DEyeLashes
@onready var sprite_addons = $Sprite2DAddons
@onready var sprite_headgear = $Sprite2DHeadgear
@onready var sprite_shield = $Sprite2DShield
@onready var sprite_shield_holding = $Sprite2DShieldHolding
@onready var sprite_weapon = $Sprite2DWeapon

# Incantation (spell casting visual)
@onready var incantation_sprite = $Incantation/IncantationSprite
@onready var animation_incantation = $Incantation/AnimationIncantation

# Attack system
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn")
var can_attack: bool = true
var is_attacking: bool = false
var is_charging_attack: bool = false
var attack_charge_time: float = 0.0
var base_attack_charge_time: float = 0.4 # Base charge time before attack
var dex: int = 10 # Dexterity stat (affects attack speed)
var blood_scene = preload("res://scenes/blood_clot.tscn")

# Bow charge visual effect (pulsing)
var bow_charge_tint: Color = Color(1.2, 1.2, 1.2, 1.0) # White tint when fully charged
var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration

# Loadout (player-like abilities) — some humanoids have bow, bomb, spell, shield, lift/throw
var has_bow: bool = false
var arrows_left: int = 0
var has_bomb: bool = false
var bombs_left: int = 0
var spell_type: String = "" # "flames" | "frost" | "healing" | ""
var has_shield: bool = false
var shield_block_chance: float = 0.0
var is_blocking: bool = false # Whether enemy is actively blocking with shield
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
var shield_block_duration: float = 1.5 # How long to block after raising shield
var can_lift_throw: bool = false
var spell_cooldown_timer: float = 0.0
var bomb_cooldown_timer: float = 0.0
var lift_throw_cooldown_timer: float = 0.0
var ranged_decision_timer: float = 0.0 # Roll for bow/bomb/spell every 0.4s
var attack_arrow_scene: PackedScene = preload("res://scenes/attack_arrow.tscn")
var attack_bomb_scene: PackedScene = preload("res://scenes/attack_bomb.tscn")
var flame_spell_scene: PackedScene = preload("res://scenes/attack_spell_flame.tscn")
var frostspike_spell_scene: PackedScene = preload("res://scenes/attack_spell_frostspike.tscn")
var interactable_object_scene: PackedScene = preload("res://scenes/interactable_object.tscn")
var held_bomb_object: Node = null # Bomb object held above enemy's head before throwing

# AI state
enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING, BOW_CHARGING, THROWING_BOMB, CASTING_SPELL, LIFTING}
var ai_state: AIState = AIState.IDLE
var state_timer: float = 0.0
var group_target: Node = null # Other humanoid to group with
var group_distance: float = 40.0 # Preferred distance from group members

# Vision system
var vision_range: float = 120.0 # How far the enemy can see (reduced from 200)
var vision_angle: float = PI * 0.75 # Field of view (135 degrees)
var patrol_speed_multiplier: float = 0.4 # Slower when patrolling (reduced from 0.6)
var aggro_range: float = 150.0 # How far enemy will chase before losing aggro (reduced from 250)
var lost_target_timer: float = 0.0 # Timer for losing target
var lost_target_duration: float = 1.0 # Time before forgetting player

# Alert indicators
@onready var alert_indicator = $AlertIndicator # Exclamation mark sprite
@onready var question_indicator = $QuestionIndicator # Question mark sprite
@onready var aggro_area = $AggroArea # Area2D for aggro detection

# Sound effects
@onready var sfx_die = $SfxDie
@onready var sfx_alert_found_player = $SfxAlertFoundPlayer
@onready var sfx_activate_shield = $SfxActivateShield
@onready var sfx_block_with_shield = $SfxBlockWithShield
@onready var sfx_bow_shoot = $SfxBowShoot
@onready var sfx_bow_without_arrow = $SfxBowWithoutArrow
@onready var sfx_buckle_bow = $SfxBuckleBow
@onready var sfx_spell_charge = $SfxSpellCharge
@onready var sfx_throw = $SfxThrow
@onready var sfx_lift = $SfxLift

# Animation system (same as player)
const ANIMATIONS = {
	"IDLE": {
		"frames": [0, 1],
		"frameDurations": [500, 500],
		"loop": true,
		"nextAnimation": null
	},
	"RUN": {
		"frames": [3, 2, 3, 4],
		"frameDurations": [140, 140, 140, 140],
		"loop": true,
		"nextAnimation": null
	},
	"SWORD": {
		"frames": [5, 6, 7, 8],
		"frameDurations": [40, 60, 90, 80],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"AXE": {
		"frames": [5, 6, 7, 8],
		"frameDurations": [50, 70, 100, 90],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"PUNCH": {
		"frames": [16, 17, 18],
		"frameDurations": [50, 70, 100],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"BOW": {
		"frames": [9, 10, 11, 12],
		"frameDurations": [80, 110, 110, 80],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"STAFF": {
		"frames": [13, 14, 15],
		"frameDurations": [200, 200, 400],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"THROW": {
		"frames": [16, 17, 18],
		"frameDurations": [80, 80, 300],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"CONJURE": {
		"frames": [19],
		"frameDurations": [400],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"DAMAGE": {
		"frames": [20, 21],
		"frameDurations": [150, 150],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"DIE": {
		"frames": [21, 22, 23, 24],
		"frameDurations": [200, 200, 200, 800],
		"loop": false,
		"nextAnimation": null
	},
	"IDLE_HOLD": {
		"frames": [25],
		"frameDurations": [500],
		"loop": true,
		"nextAnimation": null
	},
	"RUN_HOLD": {
		"frames": [25, 26, 25, 27],
		"frameDurations": [150, 150, 150, 150],
		"loop": true,
		"nextAnimation": null
	},
	"JUMP": {
		"frames": [25, 26, 27, 28],
		"frameDurations": [80, 80, 80, 800],
		"loop": false,
		"nextAnimation": "IDLE"
	},
	"LIFT": {
		"frames": [19, 30],
		"frameDurations": [70, 70],
		"loop": false,
		"nextAnimation": "IDLE_HOLD"
	},
	"IDLE_PUSH": {
		"frames": [30],
		"frameDurations": [10],
		"loop": true,
		"nextAnimation": null
	},
	"RUN_PUSH": {
		"frames": [30, 29, 30, 31],
		"frameDurations": [260, 260, 260, 260],
		"loop": true,
		"nextAnimation": null
	}
}

var current_animation = "IDLE"
var current_frame = 0
var time_since_last_frame = 0.0

# Random number generator for deterministic appearance (same on all clients)
var appearance_rng: RandomNumberGenerator

# Store spawn position for deterministic seed
var spawn_position: Vector2

func _ready():
	super._ready()
	
	# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
	collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
	
	# Override sprite reference (we use layered sprites, not single sprite)
	sprite = null # Don't use base class sprite
	
	# Store spawn position for deterministic seed (use current position if not set)
	if spawn_position == Vector2.ZERO:
		spawn_position = global_position
	
	# Create deterministic RNG for appearance
	# If randomize_appearance_on_respawn is true, add a random component to make each spawn unique
	# Otherwise, use only spawn_position and type for consistent appearance
	appearance_rng = RandomNumberGenerator.new()
	var seed_value: int
	if randomize_appearance_on_respawn:
		# Add a random component based on current time/random to make each spawn unique
		# But still deterministic across clients by using a synced random value
		var random_component = randi() # This will be different each spawn
		seed_value = hash(str(spawn_position) + str(humanoid_type) + str(random_component))
		LogManager.log(str(name) + " appearance seed (randomized): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
	else:
		# Deterministic based on position and type only
		seed_value = hash(str(spawn_position) + str(humanoid_type))
		LogManager.log(str(name) + " appearance seed (deterministic): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
	appearance_rng.seed = seed_value
	
	# Set up appearance based on type
	_setup_appearance()
	
	# Set up stats based on type
	_setup_stats()
	
	# Undead types (e.g. skeleton) take damage from healing spell
	is_undead = (humanoid_type == HumanoidType.SKELETON)
	
	# Assign loadout (bow, bomb, spell, shield, lift/throw) — variety, not experts
	_assign_loadout()
	
	# Adjust headgear for spell casters (must be after loadout assignment)
	if spell_type != "":
		# Spell caster: remove non-magician headgear and replace with magician headgear or none
		if sprite_headgear and sprite_headgear.texture:
			# Check if current headgear is NOT a magician headgear
			var current_path = sprite_headgear.texture.resource_path
			var is_magician_headgear = "Basic Mage" in current_path or current_path.ends_with("Headband.png")
			if not is_magician_headgear:
				# Remove non-magician headgear
				sprite_headgear.texture = null
				# 50% chance to add magician headgear
				if appearance_rng.randf() < 0.5:
					_load_magician_headgear()
		else:
			# No headgear currently - 50% chance to add magician headgear
			if appearance_rng.randf() < 0.5:
				_load_magician_headgear()
	
	# Update shield visibility based on loadout
	_update_shield_visibility()
	
	# Start in idle state
	ai_state = AIState.IDLE
	state_timer = 2.0

func _setup_appearance():
	# Load base body texture based on type
	var body_texture_path = _get_body_texture_for_type(humanoid_type)
	if body_texture_path:
		var body_texture = load(body_texture_path)
		if body_texture and sprite_body:
			sprite_body.texture = body_texture
			sprite_body.hframes = 35
			sprite_body.vframes = 8
	
	# Load random equipment/appearance
	_load_random_equipment()
	
	# Randomize appearance (skin, hair, facial hair, eyes, eyelashes, ears)
	_randomize_appearance()
	
	# Load type-specific addons (may override ears from randomization)
	_load_type_addons()
	
	# Load random beastkin addons (low chance, can override type addon)
	if appearance_rng.randf() < 0.1: # 10% chance
		_load_beastkin_addon()

func _randomize_appearance():
	# Randomize skin (for humanoids only - other types already have specific skins)
	if humanoid_type == HumanoidType.HUMANOID:
		var skin_value = appearance_rng.randi_range(0, 6) # 0-6 for Human1-Human7
		_set_skin(skin_value)
	
	# Randomize hair (0-12, 0 = no hair)
	var hair_type = appearance_rng.randi_range(0, 12)
	_set_hair(hair_type)
	
	# Randomize facial hair (0-3, 0 = no facial hair)
	var facial_hair_type = appearance_rng.randi_range(0, 3)
	_set_facial_hair(facial_hair_type)
	
	# Randomize eyes (1-14, 1-14 are eye colors)
	var eye_type = appearance_rng.randi_range(1, 14)
	_set_eyes(eye_type)
	
	# Randomize eyelashes (0-8, 0 = no eyelashes)
	var eyelash_type = appearance_rng.randi_range(0, 8)
	_set_eye_lashes(eyelash_type)
	
	# Randomize ears (0-7, 0 = no ears, 1-7 are elf ears)
	# Note: Type-specific addons may override this
	if humanoid_type == HumanoidType.HUMANOID:
		# Only randomize ears for regular humanoids (other types have type-specific ears)
		var ear_type = appearance_rng.randi_range(0, 7)
		_set_ears(ear_type)
	
	# Randomize hair color - vibrant and weird colors! (same as players)
	var hair_colors = [
		Color.WHITE, Color.BLACK, Color(0.4, 0.2, 0.1), # Brown
		Color(0.8, 0.6, 0.4), # Blonde
		Color(0.6, 0.3, 0.1), # Dark brown
		Color(0.9, 0.7, 0.5), # Light blonde
		Color(0.2, 0.2, 0.2), # Dark gray
		Color(0.5, 0.5, 0.5), # Gray
		Color(0.5, 0.8, 0.2), # Snot green
		Color(0.9, 0.5, 0.1), # Orange
		Color(0.8, 0.3, 0.9), # Purple
		Color(1.0, 0.9, 0.2), # Yellow
		Color(1.0, 0.5, 0.8), # Pink
		Color(0.9, 0.2, 0.2), # Red
		Color(0.2, 0.9, 0.9), # Bright cyan
		Color(0.6, 0.2, 0.9), # Magenta
		Color(0.9, 0.7, 0.2), # Gold
		Color(0.3, 0.9, 0.3), # Bright green
		Color(0.2, 0.2, 0.9), # Bright blue
		Color(0.9, 0.4, 0.6), # Hot pink
		Color(0.5, 0.2, 0.8), # Deep purple
		Color(0.9, 0.6, 0.1) # Amber
	]
	var hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
	_set_hair_color(hair_color)
	
	# Set facial hair color - usually matches hair, but can be different (30% chance)
	var facial_hair_color = hair_color
	if facial_hair_type > 0: # Only set color if they have facial hair
		if appearance_rng.randf() < 0.3: # 30% chance for different color
			facial_hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
		_set_facial_hair_color(facial_hair_color)
	else:
		# No facial hair, but still set a default color (won't be visible)
		_set_facial_hair_color(hair_color)

func _set_skin(i_value: int):
	if i_value < 0 or i_value > 6:
		return
	if sprite_body:
		var skin_path = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human" + str(i_value + 1) + ".png"
		var skin_texture = load(skin_path)
		if skin_texture:
			sprite_body.texture = skin_texture
			sprite_body.hframes = 35
			sprite_body.vframes = 8

func _set_facial_hair(i_type: int):
	if i_type < 0 or i_type > 3:
		return
	
	if not sprite_facial_hair:
		return
	
	if i_type == 0:
		sprite_facial_hair.texture = null
		return
	
	var facial_hair_path = ""
	match i_type:
		1:
			facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Beardstyle1White.png"
		2:
			facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Beardstyle2White.png"
		3:
			facial_hair_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Facial Hairstyles/Mustache1White.png"
	
	if facial_hair_path != "":
		var facial_hair_texture = load(facial_hair_path)
		if facial_hair_texture:
			sprite_facial_hair.texture = facial_hair_texture
			sprite_facial_hair.hframes = 35
			sprite_facial_hair.vframes = 8

func _set_hair(i_type: int):
	if i_type < 0 or i_type > 12:
		return
	
	if not sprite_hair:
		return
	
	if i_type == 0:
		sprite_hair.texture = null
		return
	
	var hairstyle_path = ""
	if i_type >= 1 and i_type <= 4:
		# Female hairstyles
		hairstyle_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle" + str(i_type) + "White.png"
	elif i_type >= 5:
		# Male hairstyles (5-12 maps to MHairstyle1-8)
		var male_hairstyle = i_type - 4
		hairstyle_path = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/MHairstyle" + str(male_hairstyle) + "White.png"
	
	if hairstyle_path != "":
		var hair_texture = load(hairstyle_path)
		if hair_texture:
			sprite_hair.texture = hair_texture
			sprite_hair.hframes = 35
			sprite_hair.vframes = 8
			# Apply hair color after loading texture (in case it was set before)
			# Note: This will be set in _randomize_appearance, but ensure it's applied here too
			if sprite_hair.modulate != Color.WHITE:
				_set_hair_color(sprite_hair.modulate)

func _set_eye_lashes(i_eyelashes: int):
	if i_eyelashes < 0 or i_eyelashes > 8:
		return
	
	if not sprite_eyelashes:
		return
	
	if i_eyelashes == 0:
		sprite_eyelashes.texture = null
		return
	
	var eyelash_path = ""
	match i_eyelashes:
		1:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash1.png"
		2:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash2.png"
		3:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/FEyelash3.png"
		4:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/MEyelash1.png"
		5:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/MEyelash2.png"
		6:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash1.png"
		7:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash2.png"
		8:
			eyelash_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eyelashes/NEyelash3.png"
	
	if eyelash_path != "":
		var eyelash_texture = load(eyelash_path)
		if eyelash_texture:
			sprite_eyelashes.texture = eyelash_texture
			sprite_eyelashes.hframes = 35
			sprite_eyelashes.vframes = 8

func _set_eyes(i_eyes: int):
	if i_eyes < 0 or i_eyes > 14:
		return
	
	if not sprite_eyes:
		return
	
	if i_eyes == 0:
		sprite_eyes.texture = null
		return
	
	var eye_path = ""
	match i_eyes:
		1:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorBlack.png"
		2:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorBlue.png"
		3:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorCyan.png"
		4:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkBlue.png"
		5:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkCyan.png"
		6:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkLime.png"
		7:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorDarkRed.png"
		8:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorFullBlack.png"
		9:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorFullWhite.png"
		10:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorGray.png"
		11:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorLightLime.png"
		12:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorOrange.png"
		13:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorRed.png"
		14:
			eye_path = "res://assets/gfx/Puny-Characters/Layer 5 - Eyes/Eye Color/EyecolorYellow.png"
	
	if eye_path != "":
		var eye_texture = load(eye_path)
		if eye_texture:
			sprite_eyes.texture = eye_texture
			sprite_eyes.hframes = 35
			sprite_eyes.vframes = 8

func _set_ears(i_ears: int):
	if i_ears < 0 or i_ears > 7:
		return
	
	if not sprite_addons:
		return
	
	if i_ears == 0:
		# Don't clear addons if we're not a humanoid (other types need their addons)
		# For humanoids, clear only if no type-specific addon is needed
		if humanoid_type == HumanoidType.HUMANOID:
			# Will be set by _load_type_addons if needed, otherwise stays null
			pass
		return
	
	# Only set elf ears if we're a humanoid (other types have type-specific addons)
	if humanoid_type == HumanoidType.HUMANOID:
		var ear_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/ElfEars" + str(i_ears) + ".png"
		var ear_texture = load(ear_path)
		if ear_texture:
			sprite_addons.texture = ear_texture
			sprite_addons.hframes = 35
			sprite_addons.vframes = 8

func _set_facial_hair_color(i_color: Color):
	if not sprite_facial_hair:
		return
	# Use shader tint parameter instead of modulate (same as players)
	if sprite_facial_hair.material and sprite_facial_hair.material is ShaderMaterial:
		sprite_facial_hair.material.set_shader_parameter("tint", Vector4(i_color.r, i_color.g, i_color.b, i_color.a))
	else:
		# Fallback to modulate if no shader material
		sprite_facial_hair.modulate = i_color

func _set_hair_color(i_color: Color):
	if not sprite_hair:
		return
	# Use shader tint parameter instead of modulate (same as players)
	if sprite_hair.material and sprite_hair.material is ShaderMaterial:
		sprite_hair.material.set_shader_parameter("tint", Vector4(i_color.r, i_color.g, i_color.b, i_color.a))
	else:
		# Fallback to modulate if no shader material
		sprite_hair.modulate = i_color

func _get_body_texture_for_type(type: HumanoidType) -> String:
	match type:
		HumanoidType.CYCLOPS:
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Cyclops1.png"
		HumanoidType.DEMON:
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Demon1.png"
		HumanoidType.HUMANOID:
			# Random human skin
			var skins = ["Human1.png", "Human1_1.png", "Human2.png", "Human3.png",
						"Human4.png", "Human5.png", "Human6.png", "Human7.png"]
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/" + skins[appearance_rng.randi() % skins.size()]
		HumanoidType.NIGHTELF:
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/NightElf1.png"
		HumanoidType.GOBLIN:
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Orc1.png" # Use Orc1 for Goblin
		HumanoidType.ORC:
			var orcs = ["Orc1.png", "Orc2.png"]
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/" + orcs[appearance_rng.randi() % orcs.size()]
		HumanoidType.SKELETON:
			var skeletons = ["Skeleton1.png", "Skeleton2.png"]
			return "res://assets/gfx/Puny-Characters/Layer 0 - Skins/" + skeletons[appearance_rng.randi() % skeletons.size()]
	return ""

func _load_random_equipment():
	# Load random equipment (shoes, clothes, gloves, headgear)
	# Equipment is optional - chance to have each piece
	# Random shoes (Layer 1 - Shoes)
	if appearance_rng.randf() < 0.8: # 80% chance to have shoes
		_load_random_shoes()
	
	# Random clothes/armour (Layer 2 - Clothes)
	if appearance_rng.randf() < 0.7: # 70% chance to have clothes
		_load_random_clothes()
	
	# Random gloves (Layer 3 - Gloves)
	if appearance_rng.randf() < 0.6: # 60% chance to have gloves
		_load_random_gloves()
	
	# Random headgear (Layer 6 - Headgears)
	# Note: Spell casters will have their headgear adjusted after loadout assignment
	# For now, load random headgear (will be replaced if spell caster)
	if appearance_rng.randf() < 0.5: # 50% chance to have headgear
		_load_random_headgear()

func _load_random_shoes():
	if not sprite_boots:
		return
	
	# Available shoes
	var shoes = [
		"ShoesBrown.png",
		"ShoesMaple.png",
		"IronBoots.png"
	]
	
	var shoe_file = shoes[appearance_rng.randi() % shoes.size()]
	var shoe_path = "res://assets/gfx/Puny-Characters/Layer 1 - Shoes/" + shoe_file
	var shoe_texture = load(shoe_path)
	if shoe_texture:
		sprite_boots.texture = shoe_texture
		sprite_boots.hframes = 35
		sprite_boots.vframes = 8

func _load_random_clothes():
	if not sprite_armour:
		return
	
	# Available clothes from different categories (using actual files found)
	var clothes_categories = {
		"Basic Body": [
			"BasicRed.png", "BasicBlue.png", "BasicGreen.png",
			"BasicYellow.png", "BasicBlack.png", "BasicCyan.png",
			"BasicPurple.png"
		],
		"Tunic Body": [
			"BrownTunic.png", "LeatherTunic.png", "LimeTunic.png",
			"LinenTunic.png", "WoolTunic.png"
		],
		"Armour Body": [
			"BronzeArmour.png", "GoldArmour.png", "IronArmour.png",
			"SteelArmour.png"
		],
		"Viking Body": [
			"JarlBody.png", "KarlBody.png", "WarriorBody.png"
		],
		"Mongol Body": [
			"Cherbi.png", "Khaan.png", "Kheshig.png", "Noyon.png"
		],
		"French Body": [
			"HunterIndigo.png", "MusketeerPurple.png", "MusketeerRed.png"
		],
		"Japanese Body": [
			"NinjaBlack.png", "RoninBlue.png", "RoninRed.png",
			"SamuraiLime.png", "SamuraiPurple.png", "TravellerCyan.png",
			"YabusameBlue.png", "YabusameRed.png"
		]
	}
	
	# Choose a random category
	var category_keys = clothes_categories.keys()
	var selected_category = category_keys[appearance_rng.randi() % category_keys.size()]
	var clothes_in_category = clothes_categories[selected_category]
	var selected_cloth = clothes_in_category[appearance_rng.randi() % clothes_in_category.size()]
	
	var cloth_path = "res://assets/gfx/Puny-Characters/Layer 2 - Clothes/" + selected_category + "/" + selected_cloth
	var cloth_texture = load(cloth_path)
	if cloth_texture:
		sprite_armour.texture = cloth_texture
		sprite_armour.hframes = 35
		sprite_armour.vframes = 8

func _load_random_gloves():
	# Note: Gloves are in Layer 3, but we only have sprite_armour for clothes
	# Since gloves are typically overlaid on clothes, we could:
	# 1. Load gloves as a separate layer if we add one
	# 2. Skip gloves if we already have clothes (gloves are optional)
	# 3. Combine gloves with clothes (would require composition)
	# For now, we'll skip gloves if clothes are already loaded
	# TODO: Consider adding a separate sprite layer for gloves if needed
	if not sprite_armour:
		return
	
	# Only load gloves if we don't already have clothes
	# This prevents gloves from overriding clothes
	if sprite_armour.texture:
		return # Already have clothes, skip gloves
	
	# Available gloves
	var gloves = [
		"GlovesBrown.png",
		"GlovesLightBrown.png",
		"GlovesMaple.png",
		"IronGloves.png"
	]
	
	var glove_file = gloves[appearance_rng.randi() % gloves.size()]
	var glove_path = "res://assets/gfx/Puny-Characters/Layer 3 - Gloves/" + glove_file
	var glove_texture = load(glove_path)
	if glove_texture:
		sprite_armour.texture = glove_texture
		sprite_armour.hframes = 35
		sprite_armour.vframes = 8

func _load_magician_headgear():
	# Load magician headgear for spell casters (Hat, Wizard's Hat, Mage Hat, etc.)
	if not sprite_headgear:
		return
	
	# Available magician headgears (Basic Mage category)
	var magician_headgears = [
		"EsperHatBlue.png",
		"HighMageHatCyan.png",
		"MageHatRed.png",
		"SorcererHoodCyan.png"
	]
	
	var selected_headgear = magician_headgears[appearance_rng.randi() % magician_headgears.size()]
	var headgear_path = "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/" + selected_headgear
	
	var headgear_texture = load(headgear_path)
	if headgear_texture:
		sprite_headgear.texture = headgear_texture
		sprite_headgear.hframes = 35
		sprite_headgear.vframes = 8

func _load_random_headgear():
	if not sprite_headgear:
		return
	
	# Available headgears organized by category (using actual files found)
	var headgear_categories = {
		"": ["Headband.png"], # Direct files in Layer 6 - Headgears
		"Basic Assasin": [
			"AssasinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
		],
		"Basic Mage": [
			"EsperHatBlue.png", "HighMageHatCyan.png", "MageHatRed.png", "SorcererHoodCyan.png"
		],
		"Basic Melee": [
			"DarkKnightHelm.png", "DragonKnightHelm.png", "GruntHelm.png", "KnightHelm.png",
			"NoviceHelm.png", "PaladinHelmCyan.png", "ScoutHelmGreen.png",
			"SoldierBronzeHelmBlue.png", "SoldierBronzeHelmRed.png", "SoldierGoldHelmBlue.png",
			"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
		],
		"Basic Range": [
			"ArcherHatCyan.png", "HunterHatRed.png", "RangerHatGreen.png", "RogueHatGreen.png"
		],
		"French": ["MusketeerHatPurple.png"],
		"Japanese": [
			"DaimyoHelm.png", "NinjaBandanaBlue.png", "RoninStrawhatBlue.png",
			"SamuraiHelm.png", "ShogunHelmPurple.png", "TravellerStrawhat.png",
			"TravellerStrawhatCyan.png", "VillagerStrawhat.png", "YabusameStrawhatBlue.png"
		],
		"Mongol": [
			"CherbiHelm.png", "KhaanHelm.png", "KheshigHelm.png", "NoyonHelm.png"
		],
		"Viking": ["JarlHelm.png", "KarlHelm.png"]
	}
	
	# Choose a random category
	var category_keys = headgear_categories.keys()
	var selected_category = category_keys[appearance_rng.randi() % category_keys.size()]
	var headgears_in_category = headgear_categories[selected_category]
	var selected_headgear = headgears_in_category[appearance_rng.randi() % headgears_in_category.size()]
	
	# Build path - add subdirectory if category is not empty
	var headgear_path = "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/"
	if selected_category != "":
		headgear_path += selected_category + "/"
	headgear_path += selected_headgear
	
	var headgear_texture = load(headgear_path)
	if headgear_texture:
		sprite_headgear.texture = headgear_texture
		sprite_headgear.hframes = 35
		sprite_headgear.vframes = 8

func _load_type_addons():
	# Load type-specific addons based on requirements
	match humanoid_type:
		HumanoidType.GOBLIN, HumanoidType.ORC:
			# Must have addon: GoblinEars or OrcJaw
			var options = []
			if humanoid_type == HumanoidType.GOBLIN:
				options = ["GoblinEars1.png", "GoblinEars2.png"]
			else: # ORC
				options = ["OrcJaw1.png", "OrcJaw2.png"]
			if options.size() > 0 and sprite_addons:
				var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Orc Add-ons/" + options[appearance_rng.randi() % options.size()]
				var texture = load(addon_path)
				if texture:
					sprite_addons.texture = texture
					sprite_addons.hframes = 35
					sprite_addons.vframes = 8
					LogManager.log(str(name) + " loaded type addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)
		
		HumanoidType.SKELETON:
			# Can have (but not must) skeleton horns
			if appearance_rng.randf() < 0.5 and sprite_addons:
				var options = ["SkeletonBigHorns1.png", "SkeletonSmallHorns1.png"]
				var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Skeleton Add-ons/" + options[appearance_rng.randi() % options.size()]
				var texture = load(addon_path)
				if texture:
					sprite_addons.texture = texture
					sprite_addons.hframes = 35
					sprite_addons.vframes = 8
					LogManager.log(str(name) + " loaded skeleton horns: " + str(addon_path), LogManager.CATEGORY_ENEMY)
		
		HumanoidType.HUMANOID:
			# Ears are already set by _randomize_appearance, so skip here
			# Type-specific addons would override if needed
			pass
		
		HumanoidType.NIGHTELF:
			# Must have NightElfEars7
			if sprite_addons:
				var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Elf Add-ons/NightElfEars7.png"
				var texture = load(addon_path)
				if texture:
					sprite_addons.texture = texture
					sprite_addons.hframes = 35
					sprite_addons.vframes = 8
					LogManager.log(str(name) + " loaded night elf ears", LogManager.CATEGORY_ENEMY)
		
		HumanoidType.DEMON:
			# Can have DemonEars or DemonJaw
			if appearance_rng.randf() < 0.7 and sprite_addons:
				var options = ["DemonEars.png", "DemonJaw1.png"]
				var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Demon Add-ons/" + options[appearance_rng.randi() % options.size()]
				var texture = load(addon_path)
				if texture:
					sprite_addons.texture = texture
					sprite_addons.hframes = 35
					sprite_addons.vframes = 8
					LogManager.log(str(name) + " loaded demon addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)

func _load_beastkin_addon():
	# Load random beastkin addon (low chance, can override type addon)
	var options = [
		"Antlers1.png", "BunnyEars1.png", "BunnyEars2.png",
		"CatEars1.png", "CatEars2.png", "GoatHorns1.png",
		"GoatHorns2.png", "UnicornHorn1.png"
	]
	if sprite_addons:
		var addon_path = "res://assets/gfx/Puny-Characters/Layer 7 - Add-ons/Beastkin Add-ons/" + options[appearance_rng.randi() % options.size()]
		var texture = load(addon_path)
		if texture:
			sprite_addons.texture = texture
			sprite_addons.hframes = 35
			sprite_addons.vframes = 8
			LogManager.log(str(name) + " loaded beastkin addon: " + str(addon_path), LogManager.CATEGORY_ENEMY)

func _setup_stats():
	# Set stats based on type
	match humanoid_type:
		HumanoidType.CYCLOPS:
			max_health = 55.0 # Reduced from 100.0 for better balance
			move_speed = 40.0
			damage = 15.0
			dex = 8 # Slow, strong
			exp_reward = 25.0 # Strong enemies give more EXP
		HumanoidType.DEMON:
			max_health = 45.0 # Reduced from 80.0 for better balance
			move_speed = 45.0
			damage = 12.0
			dex = 12 # Medium speed
			exp_reward = 20.0
		HumanoidType.HUMANOID:
			max_health = 35.0 # Reduced from 60.0 for better balance
			move_speed = 50.0
			damage = 10.0
			dex = 15 # Fast, agile
			exp_reward = 15.0
		HumanoidType.NIGHTELF:
			max_health = 40.0 # Reduced from 70.0 for better balance
			move_speed = 55.0
			damage = 11.0
			dex = 18 # Very fast
			exp_reward = 18.0
		HumanoidType.GOBLIN:
			max_health = 25.0 # Reduced from 40.0 for better balance
			move_speed = 60.0
			damage = 8.0
			dex = 20 # Very fast, weak
			exp_reward = 10.0 # Weak enemies give less EXP
		HumanoidType.ORC:
			max_health = 50.0 # Reduced from 90.0 for better balance
			move_speed = 42.0
			damage = 14.0
			dex = 7 # Slow, very strong
			exp_reward = 22.0
		HumanoidType.SKELETON:
			max_health = 30.0 # Reduced from 50.0 for better balance
			move_speed = 48.0
			damage = 9.0
			dex = 14 # Medium-fast
			exp_reward = 12.0
	
	current_health = max_health
	
	# Calculate attack cooldown based on DEX (higher DEX = faster attacks)
	# Base cooldown of 1.5s, reduced by DEX (max reduction to 0.5s at DEX 20)
	var dex_multiplier = 1.0 - (dex - 5) * 0.05 # Each point of DEX above 5 reduces cooldown by 5%
	dex_multiplier = clamp(dex_multiplier, 0.33, 1.0) # Clamp between 0.33x (3x faster) and 1.0x
	attack_cooldown = 1.5 * dex_multiplier
	
	# Calculate attack charge time based on DEX (higher DEX = shorter charge)
	# Base charge of 0.4s, reduced by DEX (min charge of 0.15s at DEX 20)
	var charge_multiplier = 1.0 - (dex - 5) * 0.02 # Each point of DEX above 5 reduces charge by 2%
	charge_multiplier = clamp(charge_multiplier, 0.375, 1.0) # Clamp between 0.375x (2.67x faster) and 1.0x
	base_attack_charge_time = 0.4 * charge_multiplier
	
	LogManager.log(str(name) + " stats: DEX=" + str(dex) + " attack_cooldown=" + str(attack_cooldown) + " charge_time=" + str(base_attack_charge_time), LogManager.CATEGORY_ENEMY)
	
	# Setup alert indicators
	if alert_indicator:
		alert_indicator.visible = false
		alert_indicator.z_index = 100 # Always on top
	if question_indicator:
		question_indicator.visible = false
		question_indicator.z_index = 100 # Always on top
	
	# Setup aggro area
	if aggro_area:
		# Connect signals for aggro detection
		aggro_area.body_entered.connect(_on_aggro_area_body_entered)
		aggro_area.body_exited.connect(_on_aggro_area_body_exited)
		# Set up collision shape radius to match aggro_range
		collision_shape = aggro_area.get_node_or_null("CollisionShape2D")
		if collision_shape and collision_shape.shape:
			collision_shape.shape.radius = aggro_range

func _assign_loadout():
	# Assign bow, bomb, spell, shield, lift/throw — variety, not everyone has everything. Use RNG for consistency.
	if not appearance_rng:
		return
	# Bow: ~22% get bow, 3–5 arrows
	if appearance_rng.randf() < 0.22:
		has_bow = true
		arrows_left = appearance_rng.randi_range(3, 5)
	# Bomb: ~14% get 1–2 bombs
	if appearance_rng.randf() < 0.14:
		has_bomb = true
		bombs_left = appearance_rng.randi_range(1, 2)
	# Spell: ~20% — flames 12%, frost 6%, healing 2%
	# Note: Enemies use spell_type strings (not tome items like players)
	# "flames" = Tome of Flames spell, "frost" = Tome of Frostspike spell, "healing" = Tome of Healing spell
	var spell_roll = appearance_rng.randf()
	if spell_roll < 0.12:
		spell_type = "flames"
		LogManager.log(str(name) + " assigned flames spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
	elif spell_roll < 0.18:
		spell_type = "frost"
		LogManager.log(str(name) + " assigned frost spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
	elif spell_roll < 0.20:
		spell_type = "healing"
		LogManager.log(str(name) + " assigned healing spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
	# Shield: ~24% get shield, block chance 0.12–0.22
	if appearance_rng.randf() < 0.24:
		has_shield = true
		shield_block_chance = 0.12 + appearance_rng.randf() * 0.10
	# Lift/throw: ~12% can grab and throw liftable objects when aggro
	if appearance_rng.randf() < 0.12:
		can_lift_throw = true

func _add_aim_error(dir: Vector2, max_degrees: float) -> Vector2:
	if dir.length_squared() < 0.001:
		return dir
	var deg = randf_range(-max_degrees, max_degrees)
	return dir.rotated(deg_to_rad(deg))

func _get_nearby_liftable() -> Node:
	var gw = get_tree().get_first_node_in_group("game_world")
	if not gw:
		return null
	var entities = gw.get_node_or_null("Entities")
	if not entities:
		return null
	const MAX_DIST: float = 52.0
	var best: Node = null
	var best_d: float = MAX_DIST
	for c in entities.get_children():
		if not c.has_method("can_be_grabbed") or not c.has_method("can_be_lifted"):
			continue
		if not c.can_be_grabbed() or not c.can_be_lifted():
			continue
		if "is_broken" in c and c.is_broken:
			continue
		var d = global_position.distance_to(c.global_position)
		if d < best_d:
			best_d = d
			best = c
	return best

func _physics_process(delta):
	# Always update animation (even when dead, for death animation)
	_update_animation(delta)
	
	# Handle dead state (from parent)
	if is_dead:
		if is_knocked_back:
			knockback_time += delta
			if knockback_time >= knockback_duration:
				is_knocked_back = false
				knockback_time = 0.0
				velocity = Vector2.ZERO
			else:
				velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
				move_and_slide()
		return
	
	# Only server (authority) runs AI and physics
	if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
		return
	
	# Update attack timer and reset attack flags when cooldown is over
	if attack_timer > 0:
		attack_timer -= delta
		if attack_timer <= 0:
			can_attack = true
			is_attacking = false
	if spell_cooldown_timer > 0:
		spell_cooldown_timer -= delta
	if bomb_cooldown_timer > 0:
		bomb_cooldown_timer -= delta
	if lift_throw_cooldown_timer > 0:
		lift_throw_cooldown_timer -= delta
	if ranged_decision_timer > 0:
		ranged_decision_timer -= delta
	if shield_block_timer > 0:
		shield_block_timer -= delta
		if shield_block_timer <= 0:
			is_blocking = false
			_update_shield_visibility()
	
	# Update bow charge pulse timer when charging bow
	if ai_state == AIState.BOW_CHARGING and is_charging_attack:
		bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
		_apply_bow_charge_tint()
	elif ai_state != AIState.BOW_CHARGING:
		# Clear bow charge tint when not charging
		_clear_bow_charge_tint()
	
	# Handle knockback
	if is_knocked_back:
		knockback_time += delta
		if knockback_time >= knockback_duration:
			is_knocked_back = false
			knockback_time = 0.0
			velocity = Vector2.ZERO
		else:
			velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
	
	# Enemy AI - only if not knocked back
	if not is_knocked_back:
		_ai_behavior(delta)
	
	# Move
	move_and_slide()
	
	# Don't use contact-based attack for humanoids (they use sword projectiles)
	# _check_player_collision()  # Disabled - humanoids attack with sword projectiles instead
	
	# Sync position and animation to clients (only server sends)
	if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
		# Use game_world to send RPC instead of rpc() on node instance
		# This avoids node path resolution issues when clients haven't spawned yet
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and game_world.has_method("_sync_enemy_position"):
			# Send via game_world using enemy name/index and position for identification
			var enemy_name = name
			var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
			game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, 0, current_animation, current_frame, -1])
		else:
			# Fallback: try direct call to _sync_position (not RPC)
			_sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame)

func _ai_behavior(delta):
	# Update state timer
	state_timer -= delta
	
	# Find nearest player
	target_player = _find_nearest_player()
	
	# Find nearby humanoids for grouping
	_find_group_target()
	
	# State machine
	match ai_state:
		AIState.IDLE:
			_idle_behavior(delta)
		AIState.WANDERING:
			_wandering_behavior(delta)
		AIState.NOTICED:
			_noticed_behavior(delta)
		AIState.CHASING:
			_chasing_behavior(delta)
		AIState.ATTACKING:
			_attacking_behavior(delta)
		AIState.GROUPING:
			_grouping_behavior(delta)
		AIState.BOW_CHARGING:
			_bow_charging_behavior(delta)
		AIState.THROWING_BOMB:
			_throwing_bomb_behavior(delta)
		AIState.CASTING_SPELL:
			_casting_spell_behavior(delta)
		AIState.LIFTING:
			_lifting_behavior(delta)
	
	# Update lost target timer
	if target_player:
		var dist = global_position.distance_to(target_player.global_position)
		if dist > aggro_range:
			lost_target_timer += delta
			if lost_target_timer >= lost_target_duration:
				# Forget player and show question mark
				_forget_player()
		else:
			lost_target_timer = 0.0
	else:
		lost_target_timer = 0.0

func _idle_behavior(_delta):
	velocity = Vector2.ZERO
	
	# Set idle animation if not already in another animation
	if current_animation != "DAMAGE" and current_animation != "SWORD":
		if current_animation != "IDLE":
			_set_animation("IDLE")
	
	# Check if player is in vision
	if target_player and _is_player_in_vision(target_player):
		ai_state = AIState.NOTICED
		state_timer = 0.2 # Look at player for 0.2 seconds
		_show_alert_indicator()
		return
	
	# Check if should group
	if group_target:
		ai_state = AIState.GROUPING
		state_timer = 3.0
		return
	
	# Switch to wandering after idle
	if state_timer <= 0:
		ai_state = AIState.WANDERING
		state_timer = 2.0

func _wandering_behavior(_delta):
	# Patrolling at slower pace
	# Pick a random direction at the start of wandering
	if state_timer >= 1.9: # Pick direction at start
		var random_angle = randf() * PI * 2
		var random_dir = Vector2(cos(random_angle), sin(random_angle))
		velocity = random_dir * move_speed * patrol_speed_multiplier # Slower when patrolling
		current_direction = _get_direction_from_vector(random_dir)
		_set_animation("RUN")
	
	# Check if player enters vision while patrolling
	if target_player and _is_player_in_vision(target_player):
		ai_state = AIState.NOTICED
		state_timer = 0.2 # Look at player for 0.2 seconds
		_show_alert_indicator()
		return
	
	# Switch back to idle
	if state_timer <= 0:
		ai_state = AIState.IDLE
		state_timer = 2.0
		velocity = Vector2.ZERO

func _noticed_behavior(_delta):
	# Just noticed player - look at them and show exclamation mark
	velocity = Vector2.ZERO
	
	if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
		_hide_alert_indicators()
		ai_state = AIState.IDLE
		state_timer = 1.0
		return
	
	# Face the player
	var to_player = (target_player.global_position - global_position).normalized()
	current_direction = _get_direction_from_vector(to_player)
	
	# Set idle animation
	if current_animation != "IDLE" and current_animation != "DAMAGE":
		_set_animation("IDLE")
	
	# After 0.2 seconds, start chasing
	if state_timer <= 0:
		_hide_alert_indicators()
		ai_state = AIState.CHASING
		state_timer = 5.0

func _chasing_behavior(delta_arg):
	if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
		_hide_alert_indicators()
		ai_state = AIState.IDLE
		state_timer = 1.0
		return
	
	var dist = global_position.distance_to(target_player.global_position)
	
	if dist > aggro_range:
		pass
	
	if not _is_player_in_vision(target_player):
		ai_state = AIState.WANDERING
		state_timer = 2.0
		return
	
	var to_player = (target_player.global_position - global_position).normalized()
	
	# --- Shield blocking: raise shield when player is close and attacking ---
	if has_shield and shield_block_chance > 0:
		# Check if player is attacking (recently attacked or in melee range)
		var player_is_attacking = false
		if dist < 60.0: # Close enough that player might attack
			# Check if player is facing us and might be attacking
			if "is_attacking" in target_player and target_player.is_attacking:
				player_is_attacking = true
			# Also raise shield if player is very close (within attack range)
			if dist < 50.0:
				player_is_attacking = true
		
		# Raise shield if player is attacking or very close
		if player_is_attacking and not is_blocking:
			is_blocking = true
			shield_block_timer = shield_block_duration
			_update_shield_visibility()
			# Play shield activation sound
			if sfx_activate_shield:
				sfx_activate_shield.play()
		elif not player_is_attacking and is_blocking and shield_block_timer <= 0:
			# Lower shield if player is not attacking and timer expired
			is_blocking = false
			_update_shield_visibility()
	
	# --- Lift/throw: try grab nearby liftable when aggro ---
	if can_lift_throw and lift_throw_cooldown_timer <= 0:
		var liftable = _get_nearby_liftable()
		if liftable:
			ai_state = AIState.LIFTING
			state_timer = 2.0
			velocity = Vector2.ZERO
			current_direction = _get_direction_from_vector((liftable.global_position - global_position).normalized())
			set_meta("_lift_target", liftable)
			return
	
	# --- Ranged (bow / bomb / spell): roll periodically, poor aim / mistakes ---
	ranged_decision_timer -= delta_arg
	if ranged_decision_timer <= 0:
		ranged_decision_timer = 0.4
		# 10% mistake: skip ranged even when we would use it
		if randf() < 0.10:
			pass
		elif has_bow and arrows_left > 0 and can_attack and dist >= 72 and dist <= 190:
			if randf() < 0.18:
				ai_state = AIState.BOW_CHARGING
				is_charging_attack = true
				attack_charge_time = base_attack_charge_time * 1.2
				velocity = Vector2.ZERO
				current_direction = _get_direction_from_vector(to_player)
				bow_charge_tint_pulse_time = 0.0 # Reset pulse timer
				return
		elif has_bomb and bombs_left > 0 and bomb_cooldown_timer <= 0 and dist >= 48 and dist <= 130:
			if randf() < 0.12:
				ai_state = AIState.THROWING_BOMB
				state_timer = 2.0 # Increased timer: 1.0s to hold bomb visible, 1.0s for throw animation
				velocity = Vector2.ZERO
				current_direction = _get_direction_from_vector(to_player)
				# Create bomb object above enemy's head
				_create_held_bomb_object()
				return
		elif spell_type != "" and spell_cooldown_timer <= 0 and dist >= 56 and dist <= 145:
			if randf() < 0.10:
				ai_state = AIState.CASTING_SPELL
				state_timer = 1.4
				velocity = Vector2.ZERO
				current_direction = _get_direction_from_vector(to_player)
				return
	
	# --- Melee: close enough to attack ---
	if dist < 45.0 and can_attack and not is_charging_attack:
		# Lower shield when attacking (can't block while attacking)
		if is_blocking:
			is_blocking = false
			shield_block_timer = 0.0
			_update_shield_visibility()
		ai_state = AIState.ATTACKING
		is_charging_attack = true
		attack_charge_time = base_attack_charge_time
		velocity = Vector2.ZERO
		current_direction = _get_direction_from_vector(to_player)
		return
	
	var desired_distance = 45.0
	
	# Apply speed multiplier if blocking
	var speed_mult = 1.0
	if is_blocking:
		speed_mult = shield_block_speed_multiplier
	
	if dist > desired_distance:
		# Still too far - chase player
		velocity = to_player * move_speed * 0.8 * speed_mult # Apply blocking speed reduction
	else:
		# Close enough to attack - but only stop if we can attack soon
		# If attack is on cooldown, keep following at reduced speed to maintain distance
		if can_attack:
			# Can attack - stop and wait for attack opportunity (unless blocking, then move slowly)
			if is_blocking:
				# When blocking, move slowly to maintain position
				velocity = to_player * move_speed * 0.2 * speed_mult
			else:
				velocity = Vector2.ZERO
		else:
			# Attack on cooldown - keep moving slowly to maintain position
			# Move slightly away if too close, or maintain distance
			if dist < desired_distance * 0.8:
				# Too close - back away slightly
				velocity = - to_player * move_speed * 0.3 * speed_mult
			else:
				# Good distance - just face player (or move slowly if blocking)
				if is_blocking:
					velocity = to_player * move_speed * 0.2 * speed_mult
				else:
					velocity = Vector2.ZERO
	current_direction = _get_direction_from_vector(to_player)
	
	# Set animation based on movement and whether holding something
	var is_holding_object = (held_bomb_object != null and is_instance_valid(held_bomb_object))
	
	# When blocking, use hold animations (shield is held up)
	if is_blocking:
		if velocity.length() > 0.1:
			if current_animation != "RUN_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
				_set_animation("RUN_HOLD")
		else:
			if current_animation != "IDLE_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
				_set_animation("IDLE_HOLD")
	elif is_holding_object:
		# Holding bomb or object - use hold animations
		if velocity.length() > 0.1:
			if current_animation != "RUN_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
				_set_animation("RUN_HOLD")
		else:
			if current_animation != "IDLE_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
				_set_animation("IDLE_HOLD")
	else:
		# Not holding anything - use normal animations
		if velocity.length() > 0.1:
			if current_animation != "RUN" and current_animation != "SWORD" and current_animation != "DAMAGE":
				_set_animation("RUN")
		else:
			if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
				_set_animation("IDLE")
	
	# Give up chasing after timer (or if player leaves vision)
	if state_timer <= 0:
		ai_state = AIState.IDLE
		state_timer = 2.0

func _attacking_behavior(delta):
	velocity = Vector2.ZERO
	
	# Charge attack before performing it
	if is_charging_attack:
		attack_charge_time -= delta
		if attack_charge_time <= 0:
			# Charge complete, perform attack
			is_charging_attack = false
			if can_attack and not is_attacking:
				_perform_attack()
		else:
			# Still charging - face player during charge
			if target_player and is_instance_valid(target_player) and not target_player.is_dead:
				var to_player = (target_player.global_position - global_position).normalized()
				current_direction = _get_direction_from_vector(to_player)
			# Set idle animation during charge
			if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
				_set_animation("IDLE")
			return # Don't return to chasing yet
	
	# Return to chasing after attack completes
	# Check if attack animation is done (not in SWORD animation anymore) and cooldown is over
	var attack_animation_done = (current_animation != "SWORD")
	if state_timer <= 0 and attack_animation_done and not is_charging_attack:
		ai_state = AIState.CHASING
		state_timer = 3.0
	
	# Make sure indicators are hidden during attack
	_hide_alert_indicators()

func _grouping_behavior(_delta):
	if not group_target or not is_instance_valid(group_target):
		ai_state = AIState.IDLE
		state_timer = 1.0
		return
	
	var dist = global_position.distance_to(group_target.global_position)
	
	# Maintain group distance
	if dist > group_distance * 1.5:
		# Move towards group member
		var direction = (group_target.global_position - global_position).normalized()
		velocity = direction * move_speed * 0.7
		current_direction = _get_direction_from_vector(direction)
	else:
		# Too close, move away slightly or stop
		if dist < group_distance * 0.5:
			var direction = (global_position - group_target.global_position).normalized()
			velocity = direction * move_speed * 0.3
			current_direction = _get_direction_from_vector(direction)
		else:
			velocity = Vector2.ZERO
	
	# Check if player is nearby while grouping
	if target_player:
		var player_dist = global_position.distance_to(target_player.global_position)
		if player_dist < 150.0:
			ai_state = AIState.CHASING
			state_timer = 5.0
			return
	
	# Return to idle after grouping timer
	if state_timer <= 0:
		ai_state = AIState.IDLE
		state_timer = 2.0

func _get_attack_direction_vector() -> Vector2:
	match current_direction:
		Direction.RIGHT: return Vector2.RIGHT
		Direction.DOWN_RIGHT: return Vector2(1, 1).normalized()
		Direction.DOWN: return Vector2.DOWN
		Direction.DOWN_LEFT: return Vector2(-1, 1).normalized()
		Direction.LEFT: return Vector2.LEFT
		Direction.UP_LEFT: return Vector2(-1, -1).normalized()
		Direction.UP: return Vector2.UP
		Direction.UP_RIGHT: return Vector2(1, -1).normalized()
	return Vector2.DOWN

func _bow_charging_behavior(delta):
	velocity = Vector2.ZERO
	if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
		ai_state = AIState.CHASING
		state_timer = 2.0
		return
	var to_player = (target_player.global_position - global_position).normalized()
	current_direction = _get_direction_from_vector(to_player)
	if is_charging_attack:
		attack_charge_time -= delta
		if attack_charge_time <= 0:
			is_charging_attack = false
			_perform_bow_attack()
		else:
			if current_animation != "BOW" and current_animation != "DAMAGE":
				_set_animation("BOW")
		return
	ai_state = AIState.CHASING
	state_timer = 2.0

func _perform_bow_attack():
	if not is_multiplayer_authority():
		return
	if arrows_left <= 0 or not attack_arrow_scene:
		ai_state = AIState.CHASING
		_clear_bow_charge_tint()
		return
	
	# Fire multiple arrows in a volley (2-3 arrows)
	var num_arrows = randi_range(2, 3)
	num_arrows = min(num_arrows, arrows_left) # Don't fire more than available
	
	var base_dir = _get_attack_direction_vector()
	var charge_pct = 0.65 + randf() * 0.25
	
	# Fire arrows with slight spread
	for i in range(num_arrows):
		var dir = base_dir
		# Add spread to arrows (cone pattern)
		var spread_angle = deg_to_rad(randf_range(-12.0, 12.0)) # ±12 degrees spread
		dir = dir.rotated(spread_angle)
		# Add additional aim error
		dir = _add_aim_error(dir, randf_range(16.0, 28.0))
		
		# Slight delay between arrows for visual effect
		if i > 0:
			await get_tree().create_timer(0.08 * i).timeout
		
		var arr = attack_arrow_scene.instantiate()
		var par = get_parent()
		if par:
			par.add_child(arr)
			arr.shoot(dir, global_position, self, charge_pct)
	
	arrows_left -= num_arrows
	can_attack = false
	attack_timer = attack_cooldown * 1.4
	state_timer = attack_cooldown * 1.2
	_set_animation("BOW")
	_clear_bow_charge_tint()
	ai_state = AIState.CHASING

func _throwing_bomb_behavior(delta):
	state_timer -= delta
	velocity = Vector2.ZERO
	if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
		# Clean up held bomb if target is lost
		_cleanup_held_bomb()
		ai_state = AIState.CHASING
		state_timer = 2.0
		return
	
	# Update held bomb position above enemy's head
	if held_bomb_object and is_instance_valid(held_bomb_object):
		# Position bomb above enemy's head (offset upward)
		var head_offset = Vector2(0, -20) # Above the head
		held_bomb_object.global_position = global_position + head_offset
		# Make sure bomb is visible and on top
		if held_bomb_object.has_node("Sprite2D"):
			held_bomb_object.get_node("Sprite2D").visible = true
		held_bomb_object.z_index = 10 # Above enemy sprites
	
	# Face the player while holding bomb
	current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
	
	# First phase: Hold bomb above head (show it for at least 1.0 second)
	if state_timer > 1.0:
		# Use IDLE_HOLD animation while holding bomb (enemy is stationary)
		if current_animation != "IDLE_HOLD" and current_animation != "DAMAGE":
			_set_animation("IDLE_HOLD")
		return
	
	# Second phase: Throw the bomb (after 1.0 second of holding)
	if state_timer <= 1.0 and state_timer > 0.0:
		# Use THROW animation when actually throwing
		if current_animation != "THROW" and current_animation != "DAMAGE":
			_set_animation("THROW")
		return
	
	# Third phase: Actually throw the bomb (when timer reaches 0)
	if state_timer <= 0.0 and bombs_left > 0 and is_multiplayer_authority():
		_throw_held_bomb()
	
	# Return to chasing after throw
	ai_state = AIState.CHASING
	state_timer = 2.0

func _create_held_bomb_object():
	# Create a bomb object that will be held above enemy's head before throwing
	if not is_multiplayer_authority():
		return
	
	if not interactable_object_scene:
		push_error("ERROR: Could not load interactable_object scene!")
		return
	
	# Clean up any existing held bomb
	_cleanup_held_bomb()
	
	# Spawn bomb object above enemy's head
	var entities_node = get_parent()
	if not entities_node:
		entities_node = get_tree().get_first_node_in_group("game_world").get_node_or_null("Entities")
	if not entities_node:
		push_error("ERROR: Could not find Entities node!")
		return
	
	var bomb_obj = interactable_object_scene.instantiate()
	bomb_obj.name = "EnemyHeldBomb_" + str(Time.get_ticks_msec())
	bomb_obj.global_position = global_position + Vector2(0, -20) # Above head
	bomb_obj.z_index = 10 # Above enemy sprites
	
	# Set multiplayer authority
	if multiplayer.has_multiplayer_peer():
		bomb_obj.set_multiplayer_authority(get_multiplayer_authority())
	
	entities_node.add_child(bomb_obj)
	
	# Setup as bomb object
	bomb_obj.setup_bomb()
	
	# Disable collision so it doesn't interfere
	bomb_obj.set_collision_layer_value(2, false)
	bomb_obj.set_collision_mask_value(1, false)
	bomb_obj.set_collision_mask_value(2, false)
	bomb_obj.set_collision_mask_value(7, true) # Keep wall collision
	
	# Make sure sprite is visible
	if bomb_obj.has_node("Sprite2D"):
		bomb_obj.get_node("Sprite2D").visible = true
	
	held_bomb_object = bomb_obj
	
	print(name, " created held bomb object above head")

func _throw_held_bomb():
	# Throw the bomb that's been held above enemy's head
	if not held_bomb_object or not is_instance_valid(held_bomb_object):
		# Fallback: create bomb directly if held bomb is missing
		if bombs_left > 0 and attack_bomb_scene:
			bombs_left -= 1
			bomb_cooldown_timer = 8.0
			var fallback_offset = Vector2(randf_range(-22, 22), randf_range(-22, 22))
			var fallback_target_pos = target_player.global_position + fallback_offset
			var fallback_throw_dir = (fallback_target_pos - global_position).normalized()
			var fallback_throw_force = fallback_throw_dir * randf_range(180, 260)
			var bomb = attack_bomb_scene.instantiate()
			var par = get_parent()
			if par:
				par.add_child(bomb)
				bomb.global_position = global_position + Vector2(0, -20) # From above head
				bomb.setup(bomb.global_position, self, fallback_throw_force, true)
			# Sync fallback bomb to joiners
			var game_world = get_tree().get_first_node_in_group("game_world")
			if game_world and game_world.has_method("_rpc_to_ready_peers") and multiplayer.has_multiplayer_peer():
				var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
				var bomb_name = "EnemyBomb_" + name + "_" + str(Time.get_ticks_msec())
				game_world._rpc_to_ready_peers("_sync_enemy_throw_bomb", [bomb_name, name, enemy_index, bomb.global_position, fallback_throw_force])
		return
	
	if bombs_left <= 0:
		_cleanup_held_bomb()
		return
	
	bombs_left -= 1
	bomb_cooldown_timer = 8.0
	
	# Calculate throw direction and force
	var throw_offset = Vector2(randf_range(-22, 22), randf_range(-22, 22))
	var throw_target_pos = target_player.global_position + throw_offset
	var throw_dir = (throw_target_pos - global_position).normalized()
	var throw_force = throw_dir * randf_range(180, 260)
	
	# Convert held bomb object to thrown bomb projectile
	held_bomb_object._convert_to_bomb_projectile(self, throw_force)
	
	# Clear reference (object will be freed by _convert_to_bomb_projectile)
	held_bomb_object = null
	
	print(name, " threw bomb from above head!")

func _cleanup_held_bomb():
	# Clean up held bomb object if it exists
	if held_bomb_object and is_instance_valid(held_bomb_object):
		held_bomb_object.queue_free()
		held_bomb_object = null

func _casting_spell_behavior(delta):
	state_timer -= delta
	velocity = Vector2.ZERO
	if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
		ai_state = AIState.CHASING
		return
	if state_timer > 0.8:
		current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
		if current_animation != "CONJURE" and current_animation != "DAMAGE":
			_set_animation("CONJURE")
		return
	if state_timer <= 0.8 and is_multiplayer_authority():
		spell_cooldown_timer = 7.0
		var gw = get_tree().get_first_node_in_group("game_world")
		var base_target = target_player.global_position
		var offset = Vector2(randf_range(-20, 20), randf_range(-20, 20))
		var desired = base_target + offset
		var valid = Vector2.ZERO
		if gw and gw.has_method("_get_valid_spell_target_for_enemy"):
			valid = gw._get_valid_spell_target_for_enemy(global_position, desired, get_rid())
		if valid == Vector2.ZERO:
			valid = global_position + (desired - global_position).normalized() * 80.0
		var dmg = damage * 0.8 + character_stats.baseStats.int * 0.3 if character_stats else damage
		if spell_type == "flames" and flame_spell_scene:
			var s = flame_spell_scene.instantiate()
			var par = get_parent()
			if par:
				par.add_child(s)
				s.setup(valid, self, dmg)
		elif spell_type == "frost" and frostspike_spell_scene:
			var s = frostspike_spell_scene.instantiate()
			var par = get_parent()
			if par:
				par.add_child(s)
				s.setup(valid, self, dmg, false)
		elif spell_type == "healing":
			pass
	ai_state = AIState.CHASING
	state_timer = 2.0

func _lifting_behavior(delta):
	state_timer -= delta
	velocity = Vector2.ZERO
	var lift_target = get_meta("_lift_target") if has_meta("_lift_target") else null
	if not lift_target or not is_instance_valid(lift_target):
		remove_meta("_lift_target")
		ai_state = AIState.CHASING
		lift_throw_cooldown_timer = 6.0
		return
	if state_timer > 1.2:
		current_direction = _get_direction_from_vector((lift_target.global_position - global_position).normalized())
		if current_animation != "LIFT" and current_animation != "DAMAGE":
			_set_animation("LIFT")
		return
	if state_timer <= 1.2 and is_multiplayer_authority():
		remove_meta("_lift_target")
		lift_throw_cooldown_timer = 10.0
		if lift_target.has_method("can_be_grabbed") and lift_target.can_be_grabbed() and lift_target.has_method("on_grabbed"):
			lift_target.on_grabbed(self)
		lift_target.global_position = global_position + Vector2(0, -8)
		var to_player = (target_player.global_position - global_position).normalized() if target_player else Vector2.DOWN
		to_player = _add_aim_error(to_player, 22.0)
		var force = to_player * randf_range(180, 280)
		if lift_target.has_method("on_thrown"):
			lift_target.on_thrown(self, force)
	ai_state = AIState.CHASING
	state_timer = 2.0

func _find_group_target():
	# Find nearby humanoid enemies to group with
	var humanoids = get_tree().get_nodes_in_group("enemy")
	group_target = null
	var nearest_dist = group_distance * 3.0 # Look for group members within this range
	
	for humanoid in humanoids:
		if humanoid == self or not is_instance_valid(humanoid):
			continue
		# Check if it's a humanoid enemy (has humanoid_type property)
		if not "humanoid_type" in humanoid:
			continue # Not a humanoid
		
		var dist = global_position.distance_to(humanoid.global_position)
		if dist < nearest_dist:
			nearest_dist = dist
			group_target = humanoid

func _perform_attack():
	if not can_attack or is_attacking:
		return
	
	# Only perform attack on server (authority)
	if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
		return
	
	can_attack = false
	is_attacking = true
	is_charging_attack = false # Reset charging flag
	
	# CRITICAL: Set attack timer for cooldown (this will reset can_attack when it expires)
	attack_timer = attack_cooldown
	
	# Play attack animation
	_set_animation("SWORD")
	
	# Set state timer to allow attack animation to complete before returning to chasing
	state_timer = attack_cooldown + 0.3 # Give extra time for attack animation
	
	# Calculate attack direction
	var attack_direction = Vector2.ZERO
	match current_direction:
		Direction.RIGHT:
			attack_direction = Vector2.RIGHT
		Direction.DOWN_RIGHT:
			attack_direction = Vector2(1, 1).normalized()
		Direction.DOWN:
			attack_direction = Vector2.DOWN
		Direction.DOWN_LEFT:
			attack_direction = Vector2(-1, 1).normalized()
		Direction.LEFT:
			attack_direction = Vector2.LEFT
		Direction.UP_LEFT:
			attack_direction = Vector2(-1, -1).normalized()
		Direction.UP:
			attack_direction = Vector2.UP
		Direction.UP_RIGHT:
			attack_direction = Vector2(1, -1).normalized()
	
	# Sync attack animation to clients first
	if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
		# Use game_world to send RPC instead of rpc() on node instance
		# This avoids node path resolution issues when clients haven't spawned yet
		var game_world = get_tree().get_first_node_in_group("game_world")
		var enemy_name = name
		var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
		if game_world and game_world.has_method("_sync_enemy_attack"):
			game_world._rpc_to_ready_peers("_sync_enemy_attack", [enemy_name, enemy_index, current_direction, attack_direction])
	
	# Delay before spawning sword slash
	await get_tree().create_timer(0.15).timeout
	
	# Spawn sword projectile (only on server/authority)
	if sword_projectile_scene and is_multiplayer_authority():
		var projectile = sword_projectile_scene.instantiate()
		if projectile:
			# CRITICAL: Setup projectile with direction and owner BEFORE adding to scene
			projectile.setup(attack_direction, self)
			var spawn_offset = attack_direction * 10.0
			projectile.global_position = global_position + spawn_offset
			
			# Add to scene tree
			var parent = get_parent()
			if parent:
				parent.add_child(projectile)
			else:
				LogManager.log_error("EnemyHumanoid: ERROR - No parent node to add projectile to!", LogManager.CATEGORY_ENEMY)
				projectile.queue_free()

func _try_attack_object(obj: Node):
	# Humanoid enemies can sometimes try to attack/destroy interactable objects
	# Only try if we're not already attacking and object is destroyable
	if is_attacking or not can_attack:
		return
	
	# Only try on server/authority
	if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
		return
	
	# Check if object can be destroyed
	if obj.has_method("can_be_destroyed") and obj.can_be_destroyed():
		# 30% chance to try attacking the object (less frequent than player attacks to avoid spam)
		if randf() < 0.3:
			# Face the object
			var to_object = (obj.global_position - global_position).normalized()
			current_direction = _get_direction_from_vector(to_object)
			
			# Perform attack - sword projectile will damage the object if it hits
			# The object will handle damage from sword projectiles (sword_projectile.gd already handles this)
			_perform_attack()
			LogManager.log(str(name) + " is attacking object " + str(obj.name) + "!", LogManager.CATEGORY_ENEMY)

func _sync_attack(direction: int, attack_dir: Vector2):
	# Sync attack to clients (called by game_world, not via RPC)
	# Only process on clients (not authority)
	if not is_multiplayer_authority():
		current_direction = direction as Direction
		_set_animation("SWORD")
		
		await get_tree().create_timer(0.15).timeout
		
		if sword_projectile_scene:
			var projectile = sword_projectile_scene.instantiate()
			get_parent().add_child(projectile)
			projectile.setup(attack_dir, self)
			var spawn_offset = attack_dir * 10.0
			projectile.global_position = global_position + spawn_offset
			LogManager.log(str(name) + " performed synced attack!", LogManager.CATEGORY_ENEMY)

func _set_animation(anim_name: String):
	if anim_name in ANIMATIONS:
		current_animation = anim_name
		current_frame = 0
		time_since_last_frame = 0.0

func _update_animation(delta):
	# Update animation frame timing (even when dead, to play death animation)
	# CRITICAL: If dead, ensure DIE animation is set (don't let other animations override it)
	if is_dead:
		if current_animation != "DIE" and "DIE" in ANIMATIONS:
			LogManager.log(str(name) + " (client) forcing DIE animation in _update_animation - was: " + str(current_animation), LogManager.CATEGORY_ENEMY)
			current_animation = "DIE"
			current_frame = 0
			time_since_last_frame = 0.0
	
	# CRITICAL: Ensure we have a valid animation set
	if not current_animation in ANIMATIONS:
		LogManager.log(str(name) + " WARNING: current_animation '" + str(current_animation) + "' not in ANIMATIONS!", LogManager.CATEGORY_ENEMY)
		# Fallback to IDLE if animation is invalid (or DIE if dead)
		if is_dead and "DIE" in ANIMATIONS:
			current_animation = "DIE"
		elif "IDLE" in ANIMATIONS:
			current_animation = "IDLE"
		else:
			return # Can't update animation without valid animation
	
	time_since_last_frame += delta
	if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
		var was_attacking = (current_animation == "SWORD" and current_frame == len(ANIMATIONS[current_animation]["frames"]) - 1)
		current_frame += 1
		if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
			current_frame -= 1 # Stay on last frame
			if ANIMATIONS[current_animation]["loop"]:
				current_frame = 0
			if ANIMATIONS[current_animation]["nextAnimation"] != null and not is_dead:
				# Don't transition to next animation if dead
				current_frame = 0
				var old_animation = current_animation
				current_animation = ANIMATIONS[current_animation]["nextAnimation"]
				
				# CRITICAL: If SWORD animation just completed, reset attack flags
				if old_animation == "SWORD" and was_attacking:
					# Attack animation finished - reset attack state
					# Note: can_attack will be reset when attack_timer expires
					# But we can reset is_attacking now since animation is done
					is_attacking = false
		time_since_last_frame = 0.0
	
	# Calculate frame index (8 directions, 35 frames per direction)
	# Use sprite direction index which matches player sprite frame order
	var sprite_dir = _get_sprite_direction_index()
	var frame_index = sprite_dir * 35 + ANIMATIONS[current_animation]["frames"][current_frame]
	
	# Update all sprite layers
	if sprite_body:
		sprite_body.frame = frame_index
	if sprite_boots:
		sprite_boots.frame = frame_index
	if sprite_armour:
		sprite_armour.frame = frame_index
	if sprite_facial_hair:
		sprite_facial_hair.frame = frame_index
	if sprite_hair:
		sprite_hair.frame = frame_index
	if sprite_eyes:
		sprite_eyes.frame = frame_index
	if sprite_eyelashes:
		sprite_eyelashes.frame = frame_index
	if sprite_addons:
		sprite_addons.frame = frame_index
	if sprite_headgear:
		sprite_headgear.frame = frame_index
	if sprite_shield:
		sprite_shield.frame = frame_index
	if sprite_shield_holding:
		sprite_shield_holding.frame = frame_index
	if sprite_weapon:
		sprite_weapon.frame = frame_index

# Vision system - check if player is within vision range and angle
func _is_player_in_vision(player: Node) -> bool:
	if not player or not is_instance_valid(player) or player.is_dead:
		return false
	
	var to_player = player.global_position - global_position
	var dist = to_player.length()
	
	# Check range
	if dist > vision_range:
		return false
	
	# Check angle (field of view) - based on current facing direction
	var forward_dir = Vector2(0, 1) # Default facing down
	match current_direction:
		Direction.DOWN:
			forward_dir = Vector2(0, 1)
		Direction.DOWN_RIGHT:
			forward_dir = Vector2(1, 1).normalized()
		Direction.RIGHT:
			forward_dir = Vector2(1, 0)
		Direction.UP_RIGHT:
			forward_dir = Vector2(1, -1).normalized()
		Direction.UP:
			forward_dir = Vector2(0, -1)
		Direction.UP_LEFT:
			forward_dir = Vector2(-1, -1).normalized()
		Direction.LEFT:
			forward_dir = Vector2(-1, 0)
		Direction.DOWN_LEFT:
			forward_dir = Vector2(-1, 1).normalized()
	
	var to_player_normalized = to_player.normalized()
	var angle_to_player = forward_dir.angle_to(to_player_normalized)
	
	# Check if angle is within field of view (half angle on each side)
	if abs(angle_to_player) <= vision_angle * 0.5:
		return true
	
	return false

# Convert base class Direction enum to player sprite frame direction index
# Player sprite frames: DOWN=0, DOWN_RIGHT=1, RIGHT=2, UP_RIGHT=3, UP=4, UP_LEFT=5, LEFT=6, DOWN_LEFT=7
# Base class enum: DOWN=0, LEFT=1, RIGHT=2, UP=3, DOWN_LEFT=4, DOWN_RIGHT=5, UP_LEFT=6, UP_RIGHT=7
func _get_sprite_direction_index() -> int:
	# Map base class Direction enum to player sprite frame direction index
	match current_direction:
		Direction.DOWN:
			return 0 # DOWN -> 0
		Direction.DOWN_RIGHT:
			return 1 # DOWN_RIGHT -> 1
		Direction.RIGHT:
			return 2 # RIGHT -> 2
		Direction.UP_RIGHT:
			return 3 # UP_RIGHT -> 3
		Direction.UP:
			return 4 # UP -> 4
		Direction.UP_LEFT:
			return 5 # UP_LEFT -> 5
		Direction.LEFT:
			return 6 # LEFT -> 6
		Direction.DOWN_LEFT:
			return 7 # DOWN_LEFT -> 7
	return 0

func _update_client_visuals():
	# Update visuals on clients based on synced state
	# Update all sprite layer positions based on Z
	var y_offset = - position_z * 0.5
	for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
						 sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
						 sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
		if sprite_layer:
			sprite_layer.position.y = y_offset
	
	# Animation is updated in _update_animation which is called every frame

func _apply_bow_charge_tint():
	# Apply pulsing white tint to all sprite layers when charging bow (similar to player)
	if ai_state != AIState.BOW_CHARGING or not is_charging_attack:
		return
	
	var sprite_layers = [
		{"sprite": sprite_body, "name": "body"},
		{"sprite": sprite_boots, "name": "boots"},
		{"sprite": sprite_armour, "name": "armour"},
		{"sprite": sprite_facial_hair, "name": "facial_hair"},
		{"sprite": sprite_hair, "name": "hair"},
		{"sprite": sprite_eyes, "name": "eyes"},
		{"sprite": sprite_eyelashes, "name": "eyelashes"},
		{"sprite": sprite_addons, "name": "addons"},
		{"sprite": sprite_headgear, "name": "headgear"},
		{"sprite": sprite_shield, "name": "shield"},
		{"sprite": sprite_shield_holding, "name": "shield_holding"},
		{"sprite": sprite_weapon, "name": "weapon"}
	]
	
	# Calculate pulse value (0.0 to 1.0) using sine wave
	var pulse_value = (sin(bow_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
	
	for sprite_data in sprite_layers:
		var sprite_layer = sprite_data.sprite
		var sprite_name = sprite_data.name
		
		if not sprite_layer or not is_instance_valid(sprite_layer):
			continue
		
		if sprite_layer.material and sprite_layer.material is ShaderMaterial:
			var shader_material = sprite_layer.material as ShaderMaterial
			
			# Store original tint if not already stored
			var tint_key = str(get_instance_id()) + "_bow_" + sprite_name
			if not tint_key in original_sprite_tints:
				var original_tint_param = shader_material.get_shader_parameter("tint")
				if original_tint_param is Vector4:
					original_sprite_tints[tint_key] = Color(original_tint_param.x, original_tint_param.y, original_tint_param.z, original_tint_param.w)
				elif original_tint_param is Color:
					original_sprite_tints[tint_key] = original_tint_param
				else:
					original_sprite_tints[tint_key] = Color.WHITE
			
			# Get original tint
			var original_tint = original_sprite_tints.get(tint_key, Color.WHITE)
			
			# Calculate fully charged tint (original * bow_charge_tint - white tint)
			var full_charged_tint = Color(
				original_tint.r * bow_charge_tint.r,
				original_tint.g * bow_charge_tint.g,
				original_tint.b * bow_charge_tint.b,
				original_tint.a * bow_charge_tint.a
			)
			
			# Interpolate between original and charged tint based on pulse
			var current_tint = original_tint.lerp(full_charged_tint, pulse_value * 0.5) # 50% pulse intensity
			shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a))

func _clear_bow_charge_tint():
	# Clear bow charge tint from all sprite layers
	var sprite_layers = [
		{"sprite": sprite_body, "name": "body"},
		{"sprite": sprite_boots, "name": "boots"},
		{"sprite": sprite_armour, "name": "armour"},
		{"sprite": sprite_facial_hair, "name": "facial_hair"},
		{"sprite": sprite_hair, "name": "hair"},
		{"sprite": sprite_eyes, "name": "eyes"},
		{"sprite": sprite_eyelashes, "name": "eyelashes"},
		{"sprite": sprite_addons, "name": "addons"},
		{"sprite": sprite_headgear, "name": "headgear"},
		{"sprite": sprite_shield, "name": "shield"},
		{"sprite": sprite_shield_holding, "name": "shield_holding"},
		{"sprite": sprite_weapon, "name": "weapon"}
	]
	
	var keys_to_remove = []
	
	for sprite_data in sprite_layers:
		var sprite_layer = sprite_data.sprite
		var sprite_name = sprite_data.name
		
		if not sprite_layer or not is_instance_valid(sprite_layer):
			continue
		
		if sprite_layer.material and sprite_layer.material is ShaderMaterial:
			var shader_material = sprite_layer.material as ShaderMaterial
			var tint_key = str(get_instance_id()) + "_bow_" + sprite_name
			
			# Restore original tint if we stored it
			if tint_key in original_sprite_tints:
				var original_tint = original_sprite_tints[tint_key]
				shader_material.set_shader_parameter("tint", Vector4(original_tint.r, original_tint.g, original_tint.b, original_tint.a))
				keys_to_remove.append(tint_key)
	
	# Clear stored tints
	for key in keys_to_remove:
		original_sprite_tints.erase(key)
	
	# Reset pulse timer
	bow_charge_tint_pulse_time = 0.0

func _update_shield_visibility() -> void:
	# Update shield sprite visibility based on whether enemy has a shield and is blocking
	if not sprite_shield or not sprite_shield_holding:
		return
	
	if not has_shield:
		sprite_shield.visible = false
		sprite_shield_holding.visible = false
		return
	
	# Show holding sprite when actively blocking, regular sprite otherwise
	if is_blocking:
		sprite_shield.visible = false
		sprite_shield_holding.visible = true
	else:
		sprite_shield.visible = true
		sprite_shield_holding.visible = false

func _flash_damage():
	# Flash all sprite layers red (override base class which uses single sprite)
	# But don't flash if dead or about to die - just play die animation
	if is_dead or current_health <= 0:
		return # Skip flash - die animation will play
	
	for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
						 sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
						 sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
		if sprite_layer:
			var tween = create_tween()
			tween.tween_property(sprite_layer, "modulate", Color.RED, 0.1)
			tween.tween_property(sprite_layer, "modulate", Color.WHITE, 0.1)

func take_damage(amount: float, from_position: Vector2, is_critical: bool = false, is_burn_damage: bool = false, apply_burn_debuff: bool = false):
	if has_shield and shield_block_chance > 0 and from_position != Vector2.ZERO and not is_burn_damage:
		var to_attacker = (from_position - global_position).normalized()
		var facing = _get_attack_direction_vector()
		# Check if attack is coming from the direction we're facing (blocking direction)
		if to_attacker.dot(facing) > 0.5 and randf() < shield_block_chance:
			# Successfully blocked - reduce damage
			amount = amount * 0.5
			# Raise shield if not already blocking
			if not is_blocking:
				is_blocking = true
				shield_block_timer = shield_block_duration
				_update_shield_visibility()
				if sfx_activate_shield:
					sfx_activate_shield.play()
			# Play block sound
			if sfx_block_with_shield:
				sfx_block_with_shield.play()
			# Face the attacker
			current_direction = _get_direction_from_vector(to_attacker) as Direction
		else:
			# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
			if not is_blocking:
				is_blocking = true
				shield_block_timer = shield_block_duration * 0.8 # Shorter duration for reactive blocking
				_update_shield_visibility()
				if sfx_activate_shield:
					sfx_activate_shield.play()
	super.take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff)

func _on_take_damage(attacker_position: Vector2 = Vector2.ZERO):
	if is_dead:
		return
	_set_animation("DAMAGE")
	if attacker_position != Vector2.ZERO:
		var direction_from_attacker = (global_position - attacker_position).normalized()
		current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction

func _play_death_animation():
	# Override to play death animation (same as player)
	# Ensure animation is set and animation state is properly initialized
	LogManager.log(str(name) + " _play_death_animation() called, is_authority: " + str(is_multiplayer_authority()) + ", current_animation: " + str(current_animation), LogManager.CATEGORY_ENEMY)
	_set_animation("DIE")
	# Force animation frame reset to ensure animation starts from beginning
	current_frame = 0
	time_since_last_frame = 0.0
	LogManager.log(str(name) + " _play_death_animation() set DIE animation, current_frame: " + str(current_frame) + ", current_animation: " + str(current_animation), LogManager.CATEGORY_ENEMY)
	
	# CRITICAL: Force immediate visual update on clients to ensure animation is visible
	# Call _update_animation with 0 delta to apply current frame immediately
	if not is_multiplayer_authority():
		_update_animation(0.0)
		LogManager.log(str(name) + " (client) forced immediate animation update after setting DIE", LogManager.CATEGORY_ENEMY)
	
	# Play death sound effect
	if sfx_die:
		for i in 12:
			var angle = randf_range(0, TAU)
			var speed = randf_range(50, 100)
			var initial_velocityZ = randf_range(50, 90)
			var b = blood_scene.instantiate() as CharacterBody2D
			b.scale = Vector2(randf_range(0.3, 2), randf_range(0.3, 2))
			b.global_position = global_position
		
			# Set initial velocities from the synchronized data
			var direction = Vector2.from_angle(angle)
			b.velocity = direction * speed
			b.velocityZ = initial_velocityZ
			get_parent().call_deferred("add_child", b)
		sfx_die.play()
	
	# Wait for death animation to finish (same duration as player: 200+200+200+800 = 1400ms = 1.4s)
	await get_tree().create_timer(1.4).timeout
	
	# Fade out all sprite layers (same as player)
	var fade_tween = create_tween()
	fade_tween.set_parallel(true)
	for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
						 sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
						 sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
		if sprite_layer:
			fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
	
	# Wait for fade to finish
	await fade_tween.finished
	
	# Loot already spawned in _die(), just remove now
	queue_free()

func _show_alert_indicator():
	# Show exclamation mark
	if alert_indicator:
		alert_indicator.visible = true
		alert_indicator.frame = 0
	if question_indicator:
		question_indicator.visible = false
	
	# Play alert sound when enemy first notices player (only if not already chasing/attacking)
	# This prevents the sound from playing repeatedly if they keep spotting the player
	if sfx_alert_found_player and ai_state != AIState.CHASING and ai_state != AIState.ATTACKING:
		sfx_alert_found_player.play()

func _show_question_indicator():
	# Show question mark
	if question_indicator:
		question_indicator.visible = true
		question_indicator.frame = 0
	if alert_indicator:
		alert_indicator.visible = false

func _hide_alert_indicators():
	# Hide both indicators
	if alert_indicator:
		alert_indicator.visible = false
	if question_indicator:
		question_indicator.visible = false

func _hide_question_mark_after_delay():
	# Hide question mark after delay
	if question_indicator:
		question_indicator.visible = false

func _on_aggro_area_body_entered(body):
	# Player entered aggro area
	if body and body.is_in_group("player") and not body.is_dead:
		# If we don't have a target or this is a new player, set as target
		if not target_player or target_player != body:
			target_player = body
			lost_target_timer = 0.0 # Reset timer when player enters aggro range

func _on_aggro_area_body_exited(body):
	# Player left aggro area
	if body and body.is_in_group("player") and target_player == body:
		# Start timer to forget player
		# Timer is handled in _ai_behavior, this just confirms they left
		pass

func _forget_player():
	# Forget the player and reset to patrol
	target_player = null
	lost_target_timer = 0.0
	_show_question_indicator()
	ai_state = AIState.WANDERING
	state_timer = 2.0
	velocity = Vector2.ZERO
	
	# Hide question mark after a short time (use call_deferred to avoid async issues)
	get_tree().create_timer(1.0).timeout.connect(_hide_question_mark_after_delay)
