extends Node2D

@export var player_scene: PackedScene

@onready var arena: Node2D = $Arena
@onready var combat_layer: Node2D = $CombatLayer
@onready var hud: CanvasLayer = $HUD

var player: NYXPlayer
var wave: int = 1
var enemies_alive: int = 0
var level: int = 1
var xp: int = 0
var xp_to_next: int = 100
var score: int = 0
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
var shake_time: float = 0.0
var shake_strength: float = 0.0
var boss: NYXEnemy
var boss_active: bool = false
var boss_defeated: bool = false
var boss_attack_timer: float = 1.4
var boss_summon_timer: float = 6.0
var title_screen_active: bool = true
var sfx: Dictionary = {}

func _ready() -> void:
	rng.randomize()
	_load_sfx()
	_draw_arena()
	hud.show_title_screen()

func _process(delta: float) -> void:
	shake_time = maxf(0.0, shake_time - delta)
	if shake_time > 0.0:
		combat_layer.position = Vector2(
			rng.randf_range(-shake_strength, shake_strength),
			rng.randf_range(-shake_strength, shake_strength)
		)
	else:
		combat_layer.position = Vector2.ZERO
		shake_strength = 0.0
	if boss_active:
		_update_boss(delta)

func _input(event: InputEvent) -> void:
	if title_screen_active and _is_start_input(event):
		_start_run()
		get_viewport().set_input_as_handled()
		return
	if event.is_action_pressed("restart"):
		get_tree().reload_current_scene()
	if event is InputEventKey:
		var key_event: InputEventKey = event as InputEventKey
		if key_event.pressed and not key_event.echo and key_event.keycode == KEY_B:
			_skip_to_boss()

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("restart"):
		get_tree().reload_current_scene()

func _is_start_input(event: InputEvent) -> bool:
	if event is InputEventMouseButton:
		var mouse_event: InputEventMouseButton = event as InputEventMouseButton
		return mouse_event.pressed
	if event is InputEventKey:
		var key_event: InputEventKey = event as InputEventKey
		return key_event.pressed and not key_event.echo and (
			key_event.keycode == KEY_ENTER
			or key_event.keycode == KEY_KP_ENTER
			or key_event.keycode == KEY_SPACE
		)
	return false

func _start_run() -> void:
	title_screen_active = false
	hud.hide_title_screen()
	_play_sfx("start", -9.0, 0.96, 1.04)
	player = player_scene.instantiate()
	player.global_position = Vector2(640, 360)
	combat_layer.add_child(player)
	player.stats_changed.connect(hud.set_stats)
	player.cast_night_lash.connect(_on_night_lash)
	player.cast_crown_of_dread.connect(_on_crown_of_dread)
	player.cast_starfall.connect(_on_starfall)
	player.dash_started.connect(_on_player_dash)
	player.took_damage.connect(func() -> void:
		_screen_shake(0.13, 7.0)
		_play_sfx("hurt", -8.0, 0.92, 1.06)
	)
	player.cooldowns_changed.connect(hud.set_cooldowns)
	player.ability_failed.connect(_on_ability_failed)
	player.died.connect(_on_player_died)
	_update_progress_hud()
	_spawn_wave()

func _spawn_wave() -> void:
	if boss_active or boss_defeated:
		return
	if wave > 3:
		_spawn_boss()
		return
	hud.set_wave(wave)
	hud.set_status(_wave_status_text(wave))
	enemies_alive = _enemy_count_for_wave()
	for i in range(enemies_alive):
		_spawn_enemy(i)
	if wave % 2 == 1:
		_spawn_relic()

func _wave_status_text(current_wave: int) -> String:
	if current_wave == 1:
		return "Wave 1: Break the Oathless Shades."
	if current_wave == 2:
		return "Wave 2: River-Bound Archers enter the dark."
	if current_wave == 3:
		return "Wave 3: Censer Knights guard the Ashen Gate."
	return "Break the wardens."

func _enemy_count_for_wave() -> int:
	if wave == 1:
		return 3
	if wave == 2:
		return 4
	if wave == 3:
		return 5
	return min(4 + wave, 10)

func _skip_to_boss() -> void:
	if title_screen_active or boss_active or boss_defeated:
		return
	for node in get_tree().get_nodes_in_group("enemies"):
		if node is NYXEnemy:
			var enemy: NYXEnemy = node as NYXEnemy
			enemy.queue_free()
	wave = 4
	enemies_alive = 0
	hud.set_status("Boss test: Phlegyas called to the Ashen Gate.")
	_spawn_boss()

func _load_sfx() -> void:
	sfx = {
		"start": load("res://assets/audio/sfx/start.wav"),
		"dash": load("res://assets/audio/sfx/dash.wav"),
		"night_lash": load("res://assets/audio/sfx/night_lash.wav"),
		"spell": load("res://assets/audio/sfx/spell.wav"),
		"hit": load("res://assets/audio/sfx/hit.wav"),
		"hurt": load("res://assets/audio/sfx/hurt.wav"),
		"boss_entrance": load("res://assets/audio/sfx/boss_entrance.wav"),
		"boss_attack": load("res://assets/audio/sfx/boss_attack.wav"),
		"victory": load("res://assets/audio/sfx/victory.wav"),
	}

func _play_sfx(sound_name: String, volume_db: float = -10.0, pitch_min: float = 0.98, pitch_max: float = 1.02) -> void:
	var stream: AudioStream = sfx.get(sound_name, null) as AudioStream
	if stream == null:
		return
	var sound_player: AudioStreamPlayer = AudioStreamPlayer.new()
	sound_player.stream = stream
	sound_player.volume_db = volume_db
	sound_player.pitch_scale = rng.randf_range(pitch_min, pitch_max)
	sound_player.finished.connect(func() -> void: sound_player.queue_free())
	add_child(sound_player)
	sound_player.play()

func _spawn_enemy(index: int) -> void:
	var enemy_type: String = _enemy_type_for_wave(index)
	var enemy: NYXEnemy = _create_enemy(enemy_type)
	var angle: float = TAU * float(index) / float(maxi(1, enemies_alive))
	var lane_offset: float = -44.0 if index % 2 == 0 else 44.0
	var radius: float = 235.0 + float(index % 3) * 44.0
	var spawn_direction: Vector2 = Vector2(cos(angle), sin(angle))
	var tangent: Vector2 = Vector2(-spawn_direction.y, spawn_direction.x)
	enemy.global_position = Vector2(640, 360) + spawn_direction * radius + tangent * lane_offset
	enemy.target = player
	enemy.died.connect(_on_enemy_died)
	enemy.health_changed.connect(_on_enemy_health_changed)
	enemy.attack_warning.connect(_on_enemy_attack_warning)
	enemy.ranged_warning.connect(_on_enemy_ranged_warning)
	combat_layer.add_child(enemy)

func _enemy_type_for_wave(index: int) -> String:
	if wave >= 3 and index % 5 == 4:
		return "knight"
	if wave >= 2 and index % 3 == 2:
		return "archer"
	return "shade"

func _create_enemy(enemy_type: String) -> NYXEnemy:
	var enemy: NYXEnemy = NYXEnemy.new()
	enemy.enemy_type = enemy_type
	enemy.collision_layer = 2
	enemy.collision_mask = 0
	enemy.name = _enemy_name(enemy_type)
	enemy.add_to_group("enemies")
	if enemy_type == "archer":
		enemy.speed = 128.0
		enemy.max_health = 48.0
		enemy.damage = 18.0
		enemy.attack_range = 430.0
		enemy.attack_cooldown = 1.65
		enemy.attack_windup = 0.62
		enemy.separation_radius = 74.0
	elif enemy_type == "knight":
		enemy.speed = 92.0
		enemy.max_health = 135.0
		enemy.damage = 28.0
		enemy.attack_range = 58.0
		enemy.attack_cooldown = 1.85
		enemy.attack_windup = 0.82
		enemy.fear_resistance = 0.42
		enemy.separation_radius = 92.0
		enemy.separation_force = 170.0
	elif enemy_type == "boss":
		enemy.speed = 82.0
		enemy.max_health = 760.0
		enemy.damage = 36.0
		enemy.attack_range = 112.0
		enemy.attack_cooldown = 2.4
		enemy.attack_windup = 0.85
		enemy.fear_resistance = 0.16
		enemy.separation_radius = 138.0
		enemy.separation_force = 220.0
	else:
		enemy.max_health = 54.0

	var collision: CollisionShape2D = CollisionShape2D.new()
	var shape: CircleShape2D = CircleShape2D.new()
	shape.radius = _enemy_collision_radius(enemy_type)
	collision.shape = shape
	enemy.add_child(collision)

	var body: Polygon2D = Polygon2D.new()
	body.color = _enemy_body_color(enemy_type)
	body.visible = not _enemy_uses_reference_sprite(enemy_type)
	if enemy_type == "archer":
		body.polygon = _polygon_points([
			Vector2(0, -27),
			Vector2(17, -8),
			Vector2(10, 24),
			Vector2(-10, 24),
			Vector2(-17, -8),
		])
	elif enemy_type == "knight":
		body.polygon = _polygon_points([
			Vector2(0, -36),
			Vector2(28, -12),
			Vector2(24, 28),
			Vector2(0, 42),
			Vector2(-24, 28),
			Vector2(-28, -12),
		])
	elif enemy_type == "boss":
		body.visible = false
		body.polygon = _polygon_points([
			Vector2(0, -58),
			Vector2(34, -32),
			Vector2(46, 8),
			Vector2(24, 54),
			Vector2(0, 70),
			Vector2(-24, 54),
			Vector2(-46, 8),
			Vector2(-34, -32),
		])
	else:
		body.polygon = _polygon_points([
			Vector2(0, -24),
			Vector2(19, 13),
			Vector2(0, 24),
			Vector2(-19, 13),
		])
	enemy.add_child(body)

	var eye: Polygon2D = Polygon2D.new()
	eye.color = _enemy_eye_color(enemy_type)
	eye.visible = not _enemy_uses_reference_sprite(enemy_type)
	eye.polygon = _polygon_points([
		Vector2(-8, -3),
		Vector2(8, -3),
		Vector2(0, 5),
	])
	enemy.add_child(eye)

	if _enemy_uses_reference_sprite(enemy_type):
		_add_enemy_reference_sprite(enemy, enemy_type)
		_add_enemy_readability_cue(enemy, enemy_type)

	if enemy_type == "archer":
		var bow: Line2D = Line2D.new()
		bow.width = 3.0
		bow.default_color = Color(0.76, 0.95, 1.0, 0.9)
		bow.points = PackedVector2Array([Vector2(-22, -6), Vector2(-30, 8), Vector2(-22, 22)])
		enemy.add_child(bow)
	elif enemy_type == "knight":
		var censer: Line2D = Line2D.new()
		censer.width = 5.0
		censer.default_color = Color(0.88, 0.68, 0.28, 0.95)
		censer.points = PackedVector2Array([Vector2(22, -6), Vector2(42, 18), Vector2(30, 34)])
		enemy.add_child(censer)

		var smoke: Line2D = Line2D.new()
		smoke.width = 3.0
		smoke.default_color = Color(0.2, 0.23, 0.25, 0.65)
		smoke.points = PackedVector2Array([Vector2(30, 34), Vector2(42, 20), Vector2(35, 8)])
		enemy.add_child(smoke)
	elif enemy_type == "boss":
		var boss_aura: Line2D = Line2D.new()
		boss_aura.name = "BossAura"
		boss_aura.width = 4.0
		boss_aura.default_color = Color(0.82, 0.18, 1.0, 0.44)
		boss_aura.z_index = -1
		var aura_points: PackedVector2Array = PackedVector2Array()
		for i in range(49):
			var aura_angle: float = TAU * float(i) / 48.0
			aura_points.append(Vector2(cos(aura_angle), sin(aura_angle)) * 76.0)
		boss_aura.points = aura_points
		boss_aura.closed = true
		enemy.add_child(boss_aura)

		var crown: Line2D = Line2D.new()
		crown.width = 5.0
		crown.default_color = Color(1.0, 0.78, 0.28, 0.95)
		crown.points = PackedVector2Array([
			Vector2(-28, -50),
			Vector2(-12, -70),
			Vector2(0, -54),
			Vector2(12, -70),
			Vector2(28, -50),
		])
		enemy.add_child(crown)

		var chain_left: Line2D = Line2D.new()
		chain_left.width = 6.0
		chain_left.default_color = Color(0.78, 0.2, 0.16, 0.9)
		chain_left.points = PackedVector2Array([Vector2(-38, -8), Vector2(-78, 22), Vector2(-54, 56)])
		enemy.add_child(chain_left)

		var chain_right: Line2D = Line2D.new()
		chain_right.width = 6.0
		chain_right.default_color = Color(0.78, 0.2, 0.16, 0.9)
		chain_right.points = PackedVector2Array([Vector2(38, -8), Vector2(82, 20), Vector2(56, 58)])
		enemy.add_child(chain_right)

		var oath_core: Polygon2D = Polygon2D.new()
		oath_core.color = Color(1.0, 0.18, 0.08, 0.96)
		oath_core.polygon = _polygon_points([
			Vector2(0, -14),
			Vector2(14, 0),
			Vector2(0, 16),
			Vector2(-14, 0),
		])
		enemy.add_child(oath_core)

	var health_bar_back: Line2D = Line2D.new()
	health_bar_back.name = "HealthBarBack"
	health_bar_back.width = 7.0 if enemy_type == "boss" else 4.0
	health_bar_back.default_color = Color(0.06, 0.02, 0.04, 0.75)
	health_bar_back.points = PackedVector2Array([Vector2(-22, -32), Vector2(22, -32)])
	health_bar_back.z_index = 5
	enemy.add_child(health_bar_back)

	var health_bar_fill: Line2D = Line2D.new()
	health_bar_fill.name = "HealthBarFill"
	health_bar_fill.width = 6.0 if enemy_type == "boss" else 3.0
	health_bar_fill.default_color = Color(1.0, 0.18, 0.32, 0.95)
	health_bar_fill.points = PackedVector2Array([Vector2(-22, -32), Vector2(22, -32)])
	health_bar_fill.z_index = 6
	enemy.add_child(health_bar_fill)
	return enemy

func _enemy_uses_reference_sprite(enemy_type: String) -> bool:
	return enemy_type == "shade" or enemy_type == "archer" or enemy_type == "knight" or enemy_type == "boss"

func _add_enemy_reference_sprite(enemy: NYXEnemy, enemy_type: String) -> void:
	var enemy_sprite: Sprite2D = Sprite2D.new()
	enemy_sprite.name = _enemy_sprite_name(enemy_type)
	enemy_sprite.texture = load(_enemy_sprite_path(enemy_type)) as Texture2D
	enemy_sprite.scale = _enemy_sprite_scale(enemy_type)
	enemy_sprite.position = _enemy_sprite_offset(enemy_type)
	enemy_sprite.modulate = _enemy_sprite_modulate(enemy_type)
	enemy_sprite.z_index = 2
	enemy.add_child(enemy_sprite)

func _add_enemy_readability_cue(enemy: NYXEnemy, enemy_type: String) -> void:
	var cue_color: Color = _enemy_cue_color(enemy_type)
	var ring: Line2D = Line2D.new()
	ring.name = "ReadabilityRing"
	ring.width = _enemy_ring_width(enemy_type)
	ring.default_color = cue_color
	ring.position = _enemy_ring_offset(enemy_type)
	ring.z_index = 0
	ring.points = _circle_points(_enemy_ring_radius(enemy_type), 40)
	ring.closed = true
	enemy.add_child(ring)

	var marker_points: PackedVector2Array = _enemy_marker_points(enemy_type)
	if marker_points.is_empty():
		return
	var marker: Line2D = Line2D.new()
	marker.name = "RoleMarker"
	marker.width = 4.0 if enemy_type != "boss" else 6.0
	marker.default_color = cue_color
	marker.z_index = 5
	marker.points = marker_points
	enemy.add_child(marker)

func _enemy_cue_color(enemy_type: String) -> Color:
	if enemy_type == "archer":
		return Color(0.28, 0.9, 1.0, 0.72)
	if enemy_type == "knight":
		return Color(1.0, 0.72, 0.22, 0.76)
	if enemy_type == "boss":
		return Color(1.0, 0.12, 0.08, 0.62)
	return Color(1.0, 0.16, 0.38, 0.34)

func _enemy_ring_radius(enemy_type: String) -> float:
	if enemy_type == "boss":
		return 72.0
	if enemy_type == "knight":
		return 28.0
	if enemy_type == "archer":
		return 24.0
	return 20.0

func _enemy_ring_width(enemy_type: String) -> float:
	if enemy_type == "boss":
		return 4.0
	if enemy_type == "knight":
		return 3.0
	return 2.0

func _enemy_ring_offset(enemy_type: String) -> Vector2:
	if enemy_type == "boss":
		return Vector2(0, 22)
	if enemy_type == "knight":
		return Vector2(0, 20)
	if enemy_type == "archer":
		return Vector2(0, 16)
	return Vector2(0, 14)

func _enemy_marker_points(enemy_type: String) -> PackedVector2Array:
	if enemy_type == "archer":
		return PackedVector2Array([
			Vector2(-13, -48),
			Vector2(0, -58),
			Vector2(13, -48),
		])
	if enemy_type == "knight":
		return PackedVector2Array([
			Vector2(-17, -64),
			Vector2(-8, -52),
			Vector2(0, -66),
			Vector2(8, -52),
			Vector2(17, -64),
		])
	if enemy_type == "boss":
		return PackedVector2Array([
			Vector2(-30, -92),
			Vector2(-12, -114),
			Vector2(0, -96),
			Vector2(12, -114),
			Vector2(30, -92),
		])
	return PackedVector2Array()

func _circle_points(radius: float, segments: int) -> PackedVector2Array:
	var points: PackedVector2Array = PackedVector2Array()
	for i in range(segments + 1):
		var angle: float = TAU * float(i) / float(segments)
		points.append(Vector2(cos(angle), sin(angle)) * radius)
	return points

func _enemy_sprite_name(enemy_type: String) -> String:
	if enemy_type == "archer":
		return "RiverBoundArcherSprite"
	if enemy_type == "knight":
		return "CenserKnightSprite"
	if enemy_type == "boss":
		return "PhlegyasSprite"
	return "OathlessShadeSprite"

func _enemy_sprite_path(enemy_type: String) -> String:
	if enemy_type == "archer":
		return "res://assets/sprites/enemies/river-bound-archer-gameplay-reference-v01.png"
	if enemy_type == "knight":
		return "res://assets/sprites/enemies/censer-knight-gameplay-reference-v01.png"
	if enemy_type == "boss":
		return "res://assets/sprites/enemies/phlegyas-gameplay-reference-v01.png"
	return "res://assets/sprites/enemies/oathless-shade-gameplay-reference-v01.png"

func _enemy_sprite_scale(enemy_type: String) -> Vector2:
	if enemy_type == "archer":
		return Vector2(0.058, 0.058)
	if enemy_type == "knight":
		return Vector2(0.072, 0.072)
	if enemy_type == "boss":
		return Vector2(0.13, 0.13)
	return Vector2(0.052, 0.052)

func _enemy_sprite_offset(enemy_type: String) -> Vector2:
	if enemy_type == "archer":
		return Vector2(0, -8)
	if enemy_type == "knight":
		return Vector2(0, -14)
	if enemy_type == "boss":
		return Vector2(0, -8)
	return Vector2(0, -6)

func _enemy_sprite_modulate(enemy_type: String) -> Color:
	if enemy_type == "archer":
		return Color(0.88, 0.96, 1.0, 1.0)
	if enemy_type == "knight":
		return Color(1.0, 0.93, 0.78, 1.0)
	if enemy_type == "boss":
		return Color(1.0, 0.92, 0.88, 1.0)
	return Color(1.0, 0.88, 0.92, 1.0)

func _enemy_name(enemy_type: String) -> String:
	if enemy_type == "archer":
		return "RiverBoundArcher"
	if enemy_type == "knight":
		return "CenserKnight"
	if enemy_type == "boss":
		return "PhlegyasWarden"
	return "OathlessShade"

func _enemy_collision_radius(enemy_type: String) -> float:
	if enemy_type == "boss":
		return 48.0
	if enemy_type == "knight":
		return 31.0
	if enemy_type == "archer":
		return 22.0
	return 20.0

func _enemy_body_color(enemy_type: String) -> Color:
	if enemy_type == "archer":
		return Color(0.32, 0.82, 1.0, 1.0)
	if enemy_type == "knight":
		return Color(0.76, 0.66, 0.48, 1.0)
	if enemy_type == "boss":
		return Color(0.18, 0.05, 0.08, 1.0)
	return Color(0.9, 0.12, 0.26, 1.0)

func _enemy_eye_color(enemy_type: String) -> Color:
	if enemy_type == "archer":
		return Color(0.85, 1.0, 1.0, 1.0)
	if enemy_type == "knight":
		return Color(1.0, 0.2, 0.16, 1.0)
	if enemy_type == "boss":
		return Color(1.0, 0.13, 0.08, 1.0)
	return Color(1.0, 0.85, 0.45, 1.0)

func _spawn_boss() -> void:
	boss_active = false
	boss_attack_timer = 1.5
	boss_summon_timer = 5.2
	enemies_alive = 1
	hud.set_wave(4)
	hud.set_status("The Ashen Gate trembles.")
	hud.show_boss_banner("PHLEGYAS ENTERS", "The Warden of Forgotten Oaths bars the gate.")
	_play_sfx("boss_entrance", -6.0, 0.94, 1.02)
	_draw_boss_entrance(Vector2(640, 198))
	_screen_shake(0.65, 12.0)
	await get_tree().create_timer(1.15).timeout
	hud.hide_boss_banner()
	if boss_defeated or not is_instance_valid(player):
		return
	boss_active = true
	hud.set_status("Break Phlegyas. Reclaim the First Name Fragment.")
	boss = _create_enemy("boss")
	boss.global_position = Vector2(640, 198)
	boss.target = player
	boss.died.connect(_on_enemy_died)
	boss.health_changed.connect(_on_enemy_health_changed)
	boss.attack_warning.connect(_on_enemy_attack_warning)
	boss.ranged_warning.connect(_on_enemy_ranged_warning)
	combat_layer.add_child(boss)
	hud.show_boss("Phlegyas, Warden of Forgotten Oaths", boss.health, boss.max_health)
	_play_sfx("boss_attack", -8.0, 0.82, 0.92)
	_draw_ring(boss.global_position, 210.0, Color(1.0, 0.18, 0.08, 0.34))
	_spawn_hit_sparks(boss.global_position, Color(0.78, 0.18, 1.0, 1.0), 28)
	_screen_shake(0.36, 11.0)

func _draw_boss_entrance(center: Vector2) -> void:
	_draw_ring(center, 92.0, Color(0.78, 0.18, 1.0, 0.58))
	_draw_ring(center, 170.0, Color(0.48, 0.08, 0.9, 0.38))
	_draw_ring(center, 285.0, Color(0.18, 0.02, 0.36, 0.34))
	for i in range(30):
		var angle: float = TAU * float(i) / 30.0
		var origin: Vector2 = center + Vector2(cos(angle), sin(angle)) * rng.randf_range(28.0, 95.0)
		_spawn_hit_sparks(origin, Color(0.68, 0.24, 1.0, 0.95), 1)

func _spawn_relic() -> void:
	var relic: NYXRelic = NYXRelic.new()
	relic.global_position = Vector2(rng.randf_range(220, 1060), rng.randf_range(160, 600))
	relic.collected.connect(func(_r: NYXRelic) -> void: hud.set_status("Relic claimed. Shadow restored."))

	var collision: CollisionShape2D = CollisionShape2D.new()
	var shape: CircleShape2D = CircleShape2D.new()
	shape.radius = 22.0
	collision.shape = shape
	relic.add_child(collision)

	var diamond: Polygon2D = Polygon2D.new()
	diamond.name = "Diamond"
	diamond.color = Color(1.0, 0.82, 0.36, 1.0)
	diamond.polygon = _polygon_points([
		Vector2(0, -20),
		Vector2(16, 0),
		Vector2(0, 20),
		Vector2(-16, 0),
	])
	relic.add_child(diamond)
	_add_relic_glow(relic)
	combat_layer.add_child(relic)

func _on_night_lash(origin: Vector2, direction: Vector2) -> void:
	hud.set_status("Night Lash")
	_play_sfx("night_lash", -9.0, 0.96, 1.08)
	var end: Vector2 = origin + direction * 390.0
	_draw_slash(origin, end, Color(0.34, 0.84, 1.0, 0.92))
	_screen_shake(0.06, 3.0)
	for enemy in get_tree().get_nodes_in_group("enemies"):
		if enemy is NYXEnemy:
			var typed_enemy: NYXEnemy = enemy as NYXEnemy
			if _distance_to_segment(typed_enemy.global_position, origin, end) < 48.0:
				typed_enemy.apply_damage(72.0, origin, 300.0)
				_play_sfx("hit", -13.0, 0.9, 1.12)
				_spawn_hit_sparks(typed_enemy.global_position, Color(0.7, 0.9, 1.0, 1.0), 8)

func _on_crown_of_dread(origin: Vector2) -> void:
	hud.set_status("Crown of Dread")
	_play_sfx("spell", -9.0, 0.88, 0.98)
	_draw_ring(origin, 225.0, Color(0.8, 0.92, 1.0, 0.34))
	_screen_shake(0.10, 4.0)
	for enemy in get_tree().get_nodes_in_group("enemies"):
		if enemy is NYXEnemy:
			var typed_enemy: NYXEnemy = enemy as NYXEnemy
			if typed_enemy.global_position.distance_to(origin) < 225.0:
				typed_enemy.apply_fear(2.0)
				typed_enemy.apply_damage(24.0, origin, 220.0)

func _on_starfall(_origin: Vector2, target: Vector2) -> void:
	hud.set_status("Starfall Rupture")
	_play_sfx("spell", -8.0, 1.0, 1.1)
	_draw_starfall_marker(target)
	await get_tree().create_timer(0.32).timeout
	_play_sfx("boss_attack", -11.0, 1.1, 1.22)
	_draw_ring(target, 175.0, Color(1.0, 0.82, 0.36, 0.55))
	_screen_shake(0.16, 8.0)
	for enemy in get_tree().get_nodes_in_group("enemies"):
		if enemy is NYXEnemy:
			var typed_enemy: NYXEnemy = enemy as NYXEnemy
			if typed_enemy.global_position.distance_to(target) < 175.0:
				typed_enemy.apply_damage(132.0, target, 380.0)
				_spawn_hit_sparks(typed_enemy.global_position, Color(1.0, 0.72, 0.2, 1.0), 12)

func _on_player_dash(origin: Vector2, direction: Vector2) -> void:
	hud.set_status("Eclipse Step")
	_play_sfx("dash", -10.0, 0.94, 1.08)
	_screen_shake(0.05, 2.5)
	_draw_slash(origin - direction * 34.0, origin + direction * 150.0, Color(0.25, 0.95, 1.0, 0.46))
	_spawn_dash_afterimage(origin, direction)

func _on_enemy_died(_enemy: NYXEnemy) -> void:
	_play_sfx("hit", -12.0, 0.72, 0.92)
	_spawn_hit_sparks(_enemy.global_position, Color(1.0, 0.22, 0.36, 1.0), 16)
	if _enemy == boss:
		boss_active = false
		boss_defeated = true
		enemies_alive = 0
		score += 650
		_add_xp(180)
		hud.hide_boss()
		hud.hide_boss_banner()
		hud.set_status("First Name Fragment reclaimed. Press R to replay.")
		_play_sfx("victory", -7.0, 0.98, 1.02)
		hud.show_victory(
			"FIRST NAME FRAGMENT RECLAIMED",
			"Phlegyas is broken. The Ashen Gate remembers its queen.",
			level,
			score
		)
		for other in get_tree().get_nodes_in_group("enemies"):
			if other is NYXEnemy and other != _enemy:
				var other_enemy: NYXEnemy = other as NYXEnemy
				other_enemy.queue_free()
		_draw_ring(_enemy.global_position, 240.0, Color(0.78, 0.24, 1.0, 0.46))
		_draw_ring(_enemy.global_position, 330.0, Color(0.42, 0.08, 0.8, 0.28))
		_spawn_hit_sparks(_enemy.global_position, Color(0.88, 0.54, 1.0, 1.0), 42)
		_screen_shake(0.7, 14.0)
		return
	enemies_alive -= 1
	score += 50 + wave * 10
	_add_xp(24 + wave * 4)
	if boss_active:
		_update_progress_hud()
		return
	if enemies_alive <= 0:
		wave += 1
		score += 100
		_update_progress_hud()
		await get_tree().create_timer(1.1).timeout
		_spawn_wave()

func _on_player_died() -> void:
	hud.hide_boss()
	hud.hide_boss_banner()
	hud.set_status("Nyx has fallen. Press R to restart.")

func _on_ability_failed(reason: String) -> void:
	hud.set_status(reason)

func _on_enemy_attack_warning(_enemy: NYXEnemy, target_position: Vector2, radius: float, delay: float) -> void:
	var color: Color = Color(0.95, 0.58, 0.18, 0.82) if _enemy.enemy_type == "knight" else Color(1.0, 0.16, 0.1, 0.78)
	_draw_attack_warning(target_position, radius, delay, color)

func _on_enemy_ranged_warning(_enemy: NYXEnemy, start_position: Vector2, target_position: Vector2, radius: float, delay: float) -> void:
	_draw_ranged_warning(start_position, target_position, radius, delay)

func _on_enemy_health_changed(enemy: NYXEnemy, health: float, max_health: float) -> void:
	if enemy == boss:
		hud.set_boss_health(health, max_health)

func _update_boss(delta: float) -> void:
	if not is_instance_valid(boss) or not is_instance_valid(player):
		return
	boss_attack_timer -= delta
	boss_summon_timer -= delta
	if boss_attack_timer <= 0.0:
		if rng.randi_range(0, 1) == 0:
			_boss_chain_sweep()
		else:
			_boss_oath_fire_line()
		boss_attack_timer = rng.randf_range(2.15, 2.85)
	if boss_summon_timer <= 0.0:
		_boss_summon_shades()
		boss_summon_timer = 8.0

func _boss_chain_sweep() -> void:
	if not boss_active or not is_instance_valid(boss):
		return
	var center: Vector2 = boss.global_position
	var radius: float = 148.0
	var delay: float = 0.72
	hud.set_status("Phlegyas: Chain Sweep")
	_play_sfx("boss_attack", -10.0, 0.86, 0.98)
	_draw_attack_warning(center, radius, delay, Color(1.0, 0.42, 0.12, 0.82))
	await get_tree().create_timer(delay).timeout
	if not boss_active or not is_instance_valid(boss) or not is_instance_valid(player):
		return
	_draw_ring(center, radius, Color(1.0, 0.2, 0.08, 0.55))
	_screen_shake(0.18, 8.5)
	if player.global_position.distance_to(center) <= radius:
		player.apply_damage(34.0)
		_spawn_hit_sparks(player.global_position, Color(1.0, 0.32, 0.12, 1.0), 12)

func _boss_oath_fire_line() -> void:
	if not boss_active or not is_instance_valid(boss) or not is_instance_valid(player):
		return
	var start: Vector2 = boss.global_position
	var direction: Vector2 = (player.global_position - start).normalized()
	if direction.length() <= 0.01:
		direction = Vector2.DOWN
	var end: Vector2 = start + direction * 690.0
	var width: float = 72.0
	var delay: float = 0.82
	hud.set_status("Phlegyas: Oath-Fire Line")
	_play_sfx("boss_attack", -10.0, 1.02, 1.14)
	_draw_line_warning(start, end, width, delay, Color(1.0, 0.08, 0.03, 0.72))
	await get_tree().create_timer(delay).timeout
	if not boss_active or not is_instance_valid(boss) or not is_instance_valid(player):
		return
	_draw_slash(start, end, Color(1.0, 0.12, 0.04, 0.88))
	_screen_shake(0.16, 8.0)
	if _distance_to_segment(player.global_position, start, end) <= width * 0.5:
		player.apply_damage(30.0)
		_spawn_hit_sparks(player.global_position, Color(1.0, 0.18, 0.06, 1.0), 10)

func _boss_summon_shades() -> void:
	if not boss_active or not is_instance_valid(boss):
		return
	hud.set_status("Phlegyas summons oathless dead.")
	_play_sfx("spell", -10.0, 0.72, 0.84)
	for i in range(2):
		var shade: NYXEnemy = _create_enemy("shade")
		var angle: float = rng.randf_range(0.0, TAU)
		shade.global_position = boss.global_position + Vector2(cos(angle), sin(angle)) * rng.randf_range(110.0, 165.0)
		shade.target = player
		shade.died.connect(_on_enemy_died)
		shade.health_changed.connect(_on_enemy_health_changed)
		shade.attack_warning.connect(_on_enemy_attack_warning)
		shade.ranged_warning.connect(_on_enemy_ranged_warning)
		combat_layer.add_child(shade)
		enemies_alive += 1
		_spawn_hit_sparks(shade.global_position, Color(1.0, 0.22, 0.36, 1.0), 8)

func _add_xp(amount: int) -> void:
	xp += amount
	while xp >= xp_to_next:
		xp -= xp_to_next
		level += 1
		xp_to_next += 50
		hud.set_status("Nyx grows darker. Level %d." % level)
		player.increase_max_health(18.0, 55.0)
		player.restore_shadow(100.0)
	score += amount
	_update_progress_hud()

func _update_progress_hud() -> void:
	hud.set_progress(xp, xp_to_next, level, score)

func _draw_arena() -> void:
	var bg: Polygon2D = Polygon2D.new()
	bg.color = Color(0.025, 0.012, 0.055, 1.0)
	bg.polygon = _polygon_points([
		Vector2(-2000, -1500),
		Vector2(3000, -1500),
		Vector2(3000, 2200),
		Vector2(-2000, 2200),
	])
	bg.z_index = -100
	arena.add_child(bg)

	_add_mist_cloud(Vector2(180, 120), Vector2(560, 260), Color(0.36, 0.08, 0.66, 0.13))
	_add_mist_cloud(Vector2(1060, 580), Vector2(620, 300), Color(0.24, 0.04, 0.52, 0.16))
	_add_mist_cloud(Vector2(640, 360), Vector2(780, 380), Color(0.48, 0.1, 0.72, 0.08))

	for x in range(-1200, 2400, 80):
		_add_line(Vector2(x, -900), Vector2(x + 700, 1100), Color(0.42, 0.2, 0.72, 0.14), 1.0)
	for y in range(-800, 1700, 80):
		_add_line(Vector2(-1200, y), Vector2(2200, y), Color(0.5, 0.24, 0.82, 0.1), 1.0)

	_draw_ring(Vector2(640, 360), 310.0, Color(0.58, 0.08, 0.9, 0.28), false)
	_draw_ring(Vector2(640, 360), 350.0, Color(0.22, 0.04, 0.42, 0.18), false)
	for i in range(24):
		_add_star_spark(Vector2(rng.randf_range(-80.0, 1360.0), rng.randf_range(-20.0, 760.0)))
	_add_label(Vector2(502, 90), "THE ASHEN GATE")

func _add_line(start: Vector2, end: Vector2, color: Color, width: float) -> void:
	var line: Line2D = Line2D.new()
	line.points = PackedVector2Array([start, end])
	line.default_color = color
	line.width = width
	arena.add_child(line)

func _add_label(pos: Vector2, text: String) -> void:
	var label: Label = Label.new()
	label.position = pos
	label.text = text
	label.modulate = Color(0.88, 0.62, 1.0, 0.58)
	arena.add_child(label)

func _add_mist_cloud(center: Vector2, size: Vector2, color: Color) -> void:
	var mist: Polygon2D = Polygon2D.new()
	mist.color = color
	mist.polygon = _polygon_points([
		Vector2(-0.5, -0.18) * size,
		Vector2(-0.18, -0.42) * size,
		Vector2(0.28, -0.34) * size,
		Vector2(0.54, -0.06) * size,
		Vector2(0.36, 0.32) * size,
		Vector2(-0.18, 0.42) * size,
		Vector2(-0.58, 0.16) * size,
	])
	mist.global_position = center
	arena.add_child(mist)

func _add_star_spark(pos: Vector2) -> void:
	var spark: Polygon2D = Polygon2D.new()
	var radius: float = rng.randf_range(2.0, 5.0)
	spark.color = Color(0.68, 0.28, 1.0, rng.randf_range(0.18, 0.38))
	spark.polygon = _polygon_points([
		Vector2(0, -radius),
		Vector2(radius, 0),
		Vector2(0, radius),
		Vector2(-radius, 0),
	])
	spark.global_position = pos
	arena.add_child(spark)

func _draw_slash(start: Vector2, end: Vector2, color: Color) -> void:
	var line: Line2D = Line2D.new()
	line.points = PackedVector2Array([start, end])
	line.width = 14.0
	line.default_color = color
	combat_layer.add_child(line)
	var tween: Tween = create_tween()
	tween.tween_property(line, "width", 3.0, 0.08)
	tween.parallel().tween_property(line, "modulate:a", 0.0, 0.18)
	tween.tween_callback(line.queue_free)

func _draw_ring(center: Vector2, radius: float, color: Color, temporary: bool = true) -> void:
	var line: Line2D = Line2D.new()
	var points: PackedVector2Array = PackedVector2Array()
	for i in range(65):
		var a: float = TAU * float(i) / 64.0
		points.append(center + Vector2(cos(a), sin(a)) * radius)
	line.points = points
	line.closed = true
	line.width = 4.0
	line.default_color = color
	combat_layer.add_child(line)
	if temporary:
		var tween: Tween = create_tween()
		tween.tween_property(line, "modulate:a", 0.0, 0.35)
		tween.tween_callback(line.queue_free)

func _draw_starfall_marker(center: Vector2) -> void:
	var marker: Line2D = Line2D.new()
	var points: PackedVector2Array = PackedVector2Array()
	for i in range(4):
		var angle: float = TAU * float(i) / 4.0 + PI * 0.25
		points.append(Vector2(cos(angle), sin(angle)) * 78.0)
	marker.global_position = center
	marker.points = points
	marker.closed = true
	marker.width = 3.0
	marker.default_color = Color(1.0, 0.15, 0.2, 0.82)
	combat_layer.add_child(marker)
	var tween: Tween = create_tween()
	tween.tween_property(marker, "scale", Vector2(1.55, 1.55), 0.32)
	tween.parallel().tween_property(marker, "modulate:a", 0.0, 0.32)
	tween.tween_callback(marker.queue_free)

func _draw_attack_warning(center: Vector2, radius: float, delay: float, color: Color = Color(1.0, 0.16, 0.1, 0.78)) -> void:
	var warning: Line2D = Line2D.new()
	var points: PackedVector2Array = PackedVector2Array()
	for i in range(41):
		var angle: float = TAU * float(i) / 40.0
		points.append(center + Vector2(cos(angle), sin(angle)) * radius)
	warning.points = points
	warning.closed = true
	warning.width = 3.0
	warning.default_color = color
	combat_layer.add_child(warning)
	var tween: Tween = create_tween()
	tween.tween_property(warning, "width", 9.0, delay)
	tween.parallel().tween_property(warning, "modulate:a", 0.0, delay)
	tween.tween_callback(warning.queue_free)

func _draw_ranged_warning(start: Vector2, target_point: Vector2, radius: float, delay: float) -> void:
	var aim_line: Line2D = Line2D.new()
	aim_line.points = PackedVector2Array([start, target_point])
	aim_line.width = 3.0
	aim_line.default_color = Color(0.38, 0.88, 1.0, 0.62)
	combat_layer.add_child(aim_line)
	_draw_attack_warning(target_point, radius, delay)
	var tween: Tween = create_tween()
	tween.tween_property(aim_line, "modulate:a", 0.0, delay)
	tween.tween_callback(aim_line.queue_free)
	await get_tree().create_timer(delay).timeout
	_draw_slash(start, target_point, Color(0.45, 0.9, 1.0, 0.7))

func _draw_line_warning(start: Vector2, end: Vector2, width: float, delay: float, color: Color) -> void:
	var lane: Line2D = Line2D.new()
	lane.points = PackedVector2Array([start, end])
	lane.width = width
	lane.default_color = color
	combat_layer.add_child(lane)

	var edge_a: Line2D = Line2D.new()
	var edge_b: Line2D = Line2D.new()
	var direction: Vector2 = (end - start).normalized()
	var normal: Vector2 = Vector2(-direction.y, direction.x) * width * 0.5
	edge_a.points = PackedVector2Array([start + normal, end + normal])
	edge_b.points = PackedVector2Array([start - normal, end - normal])
	edge_a.width = 3.0
	edge_b.width = 3.0
	edge_a.default_color = Color(1.0, 0.58, 0.16, 0.92)
	edge_b.default_color = Color(1.0, 0.58, 0.16, 0.92)
	combat_layer.add_child(edge_a)
	combat_layer.add_child(edge_b)

	var tween: Tween = create_tween()
	tween.tween_property(lane, "modulate:a", 0.0, delay)
	tween.parallel().tween_property(edge_a, "modulate:a", 0.0, delay)
	tween.parallel().tween_property(edge_b, "modulate:a", 0.0, delay)
	tween.tween_callback(lane.queue_free)
	tween.tween_callback(edge_a.queue_free)
	tween.tween_callback(edge_b.queue_free)

func _distance_to_segment(point: Vector2, start: Vector2, end: Vector2) -> float:
	var segment: Vector2 = end - start
	var t: float = clampf((point - start).dot(segment) / segment.length_squared(), 0.0, 1.0)
	return point.distance_to(start + segment * t)

func _polygon_points(points: Array[Vector2]) -> PackedVector2Array:
	var packed: PackedVector2Array = PackedVector2Array()
	for point in points:
		packed.append(point)
	return packed

func _screen_shake(duration: float, strength: float) -> void:
	shake_time = maxf(shake_time, duration)
	shake_strength = maxf(shake_strength, strength)

func _spawn_hit_sparks(origin: Vector2, color: Color, amount: int) -> void:
	for i in range(amount):
		var spark: Polygon2D = Polygon2D.new()
		spark.color = color
		spark.polygon = _polygon_points([
			Vector2(0, -3),
			Vector2(8, 0),
			Vector2(0, 3),
			Vector2(-8, 0),
		])
		spark.global_position = origin
		spark.rotation = rng.randf_range(0.0, TAU)
		spark.scale = Vector2.ONE * rng.randf_range(0.7, 1.3)
		combat_layer.add_child(spark)

		var travel: Vector2 = Vector2(cos(spark.rotation), sin(spark.rotation)) * rng.randf_range(22.0, 56.0)
		var tween: Tween = create_tween()
		tween.tween_property(spark, "global_position", origin + travel, 0.16)
		tween.parallel().tween_property(spark, "modulate:a", 0.0, 0.16)
		tween.tween_callback(spark.queue_free)

func _spawn_dash_afterimage(origin: Vector2, direction: Vector2) -> void:
	for i in range(4):
		var ghost: Polygon2D = Polygon2D.new()
		ghost.color = Color(0.28, 0.95, 1.0, 0.24 - float(i) * 0.04)
		ghost.polygon = _polygon_points([
			Vector2(0, -18),
			Vector2(16, 10),
			Vector2(0, 24),
			Vector2(-16, 10),
		])
		ghost.global_position = origin - direction * float(i * 28)
		ghost.rotation = direction.angle()
		combat_layer.add_child(ghost)
		var tween: Tween = create_tween()
		tween.tween_property(ghost, "modulate:a", 0.0, 0.22)
		tween.tween_callback(ghost.queue_free)

func _add_relic_glow(relic: NYXRelic) -> void:
	var glow: Line2D = Line2D.new()
	glow.name = "Glow"
	glow.width = 3.0
	glow.default_color = Color(1.0, 0.82, 0.26, 0.6)
	var points: PackedVector2Array = PackedVector2Array()
	for i in range(33):
		var angle: float = TAU * float(i) / 32.0
		points.append(Vector2(cos(angle), sin(angle)) * 30.0)
	glow.points = points
	glow.closed = true
	relic.add_child(glow)
	var tween: Tween = create_tween().set_loops(48)
	tween.tween_property(glow, "scale", Vector2(1.22, 1.22), 0.55)
	tween.tween_property(glow, "scale", Vector2.ONE, 0.55)
