extends Node3D

const PLAYER_SCENE := preload("res://scenes/Player.tscn")
const ENEMY_SCENE  := preload("res://scenes/Enemy.tscn")
const ROCK_SCENE   := preload("res://scenes/Rock.tscn")
const STICK_SCENE  := preload("res://scenes/Stick.tscn")
const LEVEL_SCENE  := preload("res://scenes/Level.tscn")

const CAM_DIST   := 8.0
const MOUSE_SENS := 0.18
const PITCH_MIN  := 5.0
const PITCH_MAX  := 70.0
const SPAWN_TIME := 5

var arena_size: float = 14.0
var cam_yaw: float    = 0.0
var cam_pitch: float  = 28.0

@onready var spawn_timer: Timer = $SpawnTimer

var player: CharacterBody3D
var camera: Camera3D
var wave: int = 1
var score: int = 0
var kills: int = 0
var kills_for_next: int = 10
var game_active: bool = false
var upgrading: bool = false

# Tutorial
var tutorial_canvas: CanvasLayer
var tutorial_image: TextureRect
var tutorial_hint: Label
var tutorial_hint_ready: bool = false
var tutorial_active: bool = false
var tutorial_on_dismiss: Callable = Callable()
var shown_tutorials: Dictionary = {}

# UI nodes
var canvas: CanvasLayer
var score_label: Label
var wave_label: Label
var tier_label: Label
var hp_bar: ColorRect
var hp_bar_bg: ColorRect
var progress_bar: ColorRect
var progress_bg: ColorRect
var upgrade_panel: Panel
var gameover_panel: Panel

# Equipment slots
var equip_fills: Array[ColorRect] = []
var equip_labels: Array[Label] = []
var _equip_prev_tiers: Array[int] = [-1, -1, -1]

# Boss phase
var boss_active: bool = false
var first_boss_spawned: bool = false
var first_bat_spawned: bool = false
var boss_timer: float = 90.0
var portal_node: Node3D = null
var boss_timer_label: Label
var boss_hint_label: Label
var win_panel: Panel

func _ready() -> void:
	_spawn_level()
	_create_camera()
	_create_ui()
	_create_tutorial_overlay()
	_spawn_player()
	_spawn_rocks()
	_spawn_sticks()
	add_to_group("main")
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	show_tutorial("Tutorial_StartGame", _start_game)

func _input(event: InputEvent) -> void:
	if tutorial_active:
		return
	var motion := event as InputEventMouseMotion
	if motion != null and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
		cam_yaw   -= motion.relative.x * MOUSE_SENS
		cam_pitch += motion.relative.y * MOUSE_SENS
		cam_pitch  = clampf(cam_pitch, PITCH_MIN, PITCH_MAX)

	if event.is_action_pressed("ui_cancel"):
		if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
			Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
		else:
			Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

	if OS.is_debug_build():
		var key := event as InputEventKey
		if key != null and key.pressed and not key.echo:
			if key.keycode == KEY_F5:
				boss_active = true
				_trigger_win()
			elif key.keycode == KEY_F6:
				_on_player_died()

# ─── Level ────────────────────────────────────────────────────────────────────

func _spawn_level() -> void:
	var level := LEVEL_SCENE.instantiate()
	add_child(level)
	var sz = level.get("arena_size")
	if sz != null:
		arena_size = float(sz)

# ─── Camera ───────────────────────────────────────────────────────────────────

func _create_camera() -> void:
	camera = Camera3D.new()
	camera.fov = 70.0
	add_child(camera)

func _process(delta: float) -> void:
	if is_instance_valid(player):
		var yaw_r: float   = deg_to_rad(cam_yaw)
		var pitch_r: float = deg_to_rad(cam_pitch)
		var offset := Vector3(
			sin(yaw_r) * cos(pitch_r) * CAM_DIST,
			sin(pitch_r) * CAM_DIST,
			cos(yaw_r) * cos(pitch_r) * CAM_DIST
		)
		var look_at_pos := player.global_position + Vector3(0, 0.8, 0)
		camera.global_position = camera.global_position.lerp(look_at_pos + offset, 14.0 * delta)
		camera.look_at(look_at_pos, Vector3.UP)
		if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
			player.set_aim_direction(deg_to_rad(cam_yaw))
		_update_tier_label()

	if boss_active:
		boss_timer = max(0.0, boss_timer - delta)
		var mins := int(boss_timer) / 60
		var secs := int(boss_timer) % 60
		boss_timer_label.text = "%d:%02d" % [mins, secs]
		if boss_timer <= 10.0:
			boss_timer_label.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
		if boss_timer <= 0.0:
			_trigger_time_up()
			return
		if is_instance_valid(portal_node):
			var area := portal_node.get_node_or_null("PortalArea") as Area3D
			if area != null:
				for body in area.get_overlapping_bodies():
					if body.get("enemy_level") == 3:
						var fv = body.get("fly_vel")
						if fv != null:
							var spd := Vector2((fv as Vector3).x, (fv as Vector3).z).length()
							if spd >= 25.0:
								_trigger_win()
								return

# ─── Rocks ────────────────────────────────────────────────────────────────────

var rocks_on_field: int = 0
var rocks_pending: int  = 0

func _get_rock_limit() -> int:
	return mini(3 + (wave + 1) / 2, 7)

func _spawn_rocks() -> void:
	var limit := _get_rock_limit()
	for i in range(limit):
		_spawn_single_rock()

var sticks_on_field: int = 0
var sticks_pending: int = 0
const STICK_LIMIT := 3

func _spawn_sticks() -> void:
	for i in range(STICK_LIMIT):
		_spawn_single_stick()

func _spawn_single_stick() -> void:
	var stick := STICK_SCENE.instantiate()
	stick.position = _safe_item_position()
	add_child(stick)
	stick.connect("destroyed", _on_stick_destroyed)
	sticks_on_field += 1

func _on_stick_destroyed() -> void:
	sticks_on_field = maxi(0, sticks_on_field - 1)
	if not game_active:
		return
	if sticks_on_field + sticks_pending < STICK_LIMIT:
		sticks_pending += 1
		await get_tree().create_timer(GameSettings.item_respawn_delay).timeout

		sticks_pending -= 1
		if game_active:
			_spawn_single_stick()

func _spawn_single_rock() -> void:
	var rock := ROCK_SCENE.instantiate()
	rock.position = _safe_item_position()
	add_child(rock)
	rock.connect("destroyed", _on_rock_destroyed)
	rocks_on_field += 1

func _safe_item_position() -> Vector3:
	var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO
	for _attempt in range(30):
		var angle := randf() * TAU
		var dist  := randf_range(4.0, arena_size - 2.0)
		var pos   := Vector3(cos(angle) * dist, 0.0, sin(angle) * dist)
		if player_pos.distance_to(pos) < 4.5:
			continue
		var clear := true
		for r in get_tree().get_nodes_in_group("kickable"):
			if (r as Node3D).global_position.distance_to(pos) < 1.5:
				clear = false
				break
		if clear:
			return pos
	var a := randf() * TAU
	return Vector3(cos(a) * (arena_size - 2.5), 0.0, sin(a) * (arena_size - 2.5))

func _on_rock_destroyed() -> void:
	rocks_on_field = maxi(0, rocks_on_field - 1)
	if not game_active:
		return
	if rocks_on_field + rocks_pending < _get_rock_limit():
		rocks_pending += 1
		await get_tree().create_timer(GameSettings.item_respawn_delay).timeout
		rocks_pending -= 1
		if game_active:
			_spawn_single_rock()

func _check_rock_slots() -> void:
	var gap := _get_rock_limit() - rocks_on_field - rocks_pending
	for i in range(gap):
		_spawn_single_rock()

# ─── Player ───────────────────────────────────────────────────────────────────

func _spawn_player() -> void:
	player = PLAYER_SCENE.instantiate() as CharacterBody3D
	player.position = Vector3(0, 0, 0)
	player.connect("died", _on_player_died)
	player.connect("health_changed", _on_health_changed)
	add_child(player)

# ─── Game flow ────────────────────────────────────────────────────────────────

func _start_game() -> void:
	SFX.start_ambient(self)
	game_active = true
	wave = 1
	score = 0
	kills = 0
	kills_for_next = 10
	Enemy.first_leather_spawned = false
	Enemy.first_iron_spawned = false
	Enemy.first_essence_spawned = false
	_update_labels()
	spawn_timer.wait_time = GameSettings.enemy_spawn_interval
	spawn_timer.connect("timeout", _on_spawn_timer)
	spawn_timer.start()

func _on_spawn_timer() -> void:
	if not game_active or upgrading:
		return
	_spawn_enemy()
	spawn_timer.wait_time = GameSettings.enemy_spawn_interval

func _spawn_enemy() -> void:
	var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D
	add_child(enemy)

	# Pick type based on wave
	var pool: Array[String] = ["slime"]
	if wave >= 4: pool.append("bat")
	if wave >= 7: pool.append("ogre")
	var type: String = pool[randi() % pool.size()]
	enemy.setup(type, wave)
	enemy.target = player
	enemy.connect("died", _on_enemy_died)
	enemy.connect("merged", _on_enemy_merged)
	if type == "bat" and not first_bat_spawned:
		first_bat_spawned = true
		show_tutorial("Tutorial_LeatherBoots")
	if type == "ogre" and not first_boss_spawned:
		first_boss_spawned = true
		_start_boss_phase()

	# Spawn at random edge
	var side := randi() % 4
	var r := randf_range(-(arena_size - 1.0), arena_size - 1.0)
	match side:
		0: enemy.position = Vector3(r, 0, -(arena_size - 0.5))
		1: enemy.position = Vector3(r, 0,  (arena_size - 0.5))
		2: enemy.position = Vector3(-(arena_size - 0.5), 0, r)
		3: enemy.position = Vector3( (arena_size - 0.5), 0, r)

func add_bonus_score(amount: int) -> void:
	score += amount
	_update_labels()

func _on_enemy_died(points: int) -> void:
	score += points
	kills += 1
	_update_labels()
	_update_progress()

	if kills >= kills_for_next:
		kills = 0
		kills_for_next = int(kills_for_next * 1.6)
		wave += 1
		_check_rock_slots()
		_show_upgrade()

func _spawn_upgraded_enemy(pos: Vector3, type: String, level: int, w: int) -> CharacterBody3D:
	var enemy := ENEMY_SCENE.instantiate() as CharacterBody3D
	add_child(enemy)
	enemy.setup(type, w)
	enemy.target = player
	enemy.enemy_level = level
	enemy.kick_tier = level
	enemy.toughness_tier = level
	enemy.call("_update_label")
	enemy.call("_apply_mesh", level)
	enemy.global_position = pos
	enemy.connect("died", _on_enemy_died)
	enemy.connect("merged", _on_enemy_merged)
	var tw := enemy.create_tween()
	var s: float = 1.0 + level * 0.3
	tw.tween_property(enemy.mesh_node, "scale", Vector3(s, s, s), 0.2)
	var col_shape := enemy.get_node_or_null("CollisionShape3D") as CollisionShape3D
	if col_shape != null and col_shape.shape != null:
		var s3d: BoxShape3D = col_shape.shape as BoxShape3D
		var old_size: Vector3 = s3d.size
		col_shape.shape = BoxShape3D.new()
		(col_shape.shape as BoxShape3D).size = old_size * s
	if level >= 3 and not first_boss_spawned:
		first_boss_spawned = true
		_start_boss_phase()
	return enemy

func _on_enemy_merged(_upgrade: bool) -> void:
	pass

func _on_player_died() -> void:
	game_active = false
	if boss_active:
		boss_active = false
		boss_timer_label.visible = false
		boss_hint_label.visible = false
		if is_instance_valid(portal_node):
			portal_node.queue_free()
	spawn_timer.stop()
	_show_gameover()

func _on_health_changed(cur: int, mx: int) -> void:
	hp_bar.size.x = 200.0 * float(cur) / float(mx)

# ─── Upgrades ─────────────────────────────────────────────────────────────────

const UPGRADES := [
	{"id": "kick_force",    "name": "💥 Stronger Kick",   "desc": "Enemies fly farther"},
	{"id": "kick_range",    "name": "🌐 Wider Kick",       "desc": "Bigger kick area"},
	{"id": "kick_cooldown", "name": "⚡ Faster Kick",      "desc": "Kick more often"},
	{"id": "move_speed",    "name": "💨 Fleet Foot",       "desc": "Move faster"},
	{"id": "max_health",    "name": "❤️  Vitality",        "desc": "+30 max HP"},
]

func _show_upgrade() -> void:
	upgrading = true
	get_tree().paused = true
	upgrade_panel.visible = true

	# Pick 3 random upgrades
	var pool: Array = UPGRADES.duplicate()
	pool.shuffle()
	var choices: Array = pool.slice(0, 3)

	for i in range(3):
		var btn := upgrade_panel.get_node("VBox/Btn%d" % i) as Button
		if i < choices.size():
			var upg: Dictionary = choices[i]
			btn.text = "%s\n%s" % [upg["name"], upg["desc"]]
			btn.visible = true
			# Disconnect old signals
			for conn in btn.get_signal_connection_list("pressed"):
				btn.disconnect("pressed", conn["callable"])
			var uid: String = upg["id"]
			btn.connect("pressed", _pick_upgrade.bind(uid))
		else:
			btn.visible = false

func _pick_upgrade(id: String) -> void:
	player.apply_upgrade(id)
	upgrade_panel.visible = false
	get_tree().paused = false
	upgrading = false
	_update_labels()

func _show_gameover() -> void:
	Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	var lbl := gameover_panel.get_node("VBox/ScoreLabel") as Label
	lbl.text = "Score: %d\nWave: %d" % [score, wave]
	show_tutorial("LoseGame", func() -> void:
		gameover_panel.visible = true
	)

func _restart() -> void:
	get_tree().paused = false
	get_tree().reload_current_scene()

# ─── Boss phase ───────────────────────────────────────────────────────────────

func _start_boss_phase() -> void:
	if boss_active or not game_active:
		return
	boss_active = true
	boss_timer = 120.0
	_spawn_portal()
	show_tutorial("ThirdLevelEnemy", func() -> void:
		boss_timer_label.visible = true
		boss_hint_label.visible = true
	)

func _spawn_portal() -> void:
	portal_node = Node3D.new()
	add_child(portal_node)

	var player_pos := player.global_position if is_instance_valid(player) else Vector3.ZERO
	var best_pos   := Vector3(arena_size - 1.5, 0.0, 0.0)
	var best_dist  := 0.0
	for _i in range(16):
		var angle := randf() * TAU
		var cand  := Vector3(cos(angle), 0.0, sin(angle)) * (arena_size - 1.5)
		var d     := cand.distance_to(player_pos)
		if d > best_dist:
			best_dist = d
			best_pos  = cand
	portal_node.global_position = best_pos

	var light := OmniLight3D.new()
	light.light_color  = Color(0.5, 0.1, 1.0)
	light.omni_range   = 7.0
	light.light_energy = 3.0
	light.position.y   = 1.0
	portal_node.add_child(light)

	var ring     := MeshInstance3D.new()
	var torus    := TorusMesh.new()
	torus.inner_radius = 1.0
	torus.outer_radius = 1.65
	var ring_mat := StandardMaterial3D.new()
	ring_mat.albedo_color     = Color(0.55, 0.1, 1.0)
	ring_mat.emission_enabled = true
	ring_mat.emission         = Color(0.6, 0.2, 1.0)
	ring_mat.emission_energy  = 3.5
	ring_mat.shading_mode     = BaseMaterial3D.SHADING_MODE_UNSHADED
	torus.material = ring_mat
	ring.mesh      = torus
	ring.position.y = 0.05
	portal_node.add_child(ring)
	ring.create_tween().set_loops().tween_property(ring, "rotation:y", TAU, 2.5)

	var disc      := MeshInstance3D.new()
	var disc_mesh := CylinderMesh.new()
	disc_mesh.top_radius    = 1.0
	disc_mesh.bottom_radius = 1.0
	disc_mesh.height        = 0.05
	var disc_mat := StandardMaterial3D.new()
	disc_mat.albedo_color     = Color(0.25, 0.0, 0.7, 0.65)
	disc_mat.shading_mode     = BaseMaterial3D.SHADING_MODE_UNSHADED
	disc_mat.transparency     = BaseMaterial3D.TRANSPARENCY_ALPHA
	disc_mat.emission_enabled = true
	disc_mat.emission         = Color(0.4, 0.1, 1.0)
	disc_mat.emission_energy  = 1.5
	disc_mesh.material = disc_mat
	disc.mesh          = disc_mesh
	disc.position.y    = 0.03
	portal_node.add_child(disc)
	var tw_pulse := portal_node.create_tween().set_loops()
	tw_pulse.tween_method(func(e: float): disc_mat.emission_energy = e, 0.8, 3.0, 0.7)
	tw_pulse.tween_method(func(e: float): disc_mat.emission_energy = e, 3.0, 0.8, 0.7)

	var area     := Area3D.new()
	area.name    = "PortalArea"
	var col      := CollisionShape3D.new()
	var cyl      := CylinderShape3D.new()
	cyl.radius   = 1.8
	cyl.height   = 3.0
	col.shape    = cyl
	col.position.y = 1.5
	area.add_child(col)
	portal_node.add_child(area)

	var lbl := Label3D.new()
	lbl.text                 = "PORTAL\nPush Ogre Here"
	lbl.position             = Vector3(0, 2.6, 0)
	lbl.billboard            = BaseMaterial3D.BILLBOARD_ENABLED
	lbl.font_size            = 34
	lbl.outline_size         = 6
	lbl.modulate             = Color(0.85, 0.55, 1.0)
	lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	portal_node.add_child(lbl)

	FX.merge_smoke(best_pos + Vector3(0, 0.5, 0), self)

func _trigger_win() -> void:
	if not boss_active:
		return
	boss_active = false
	game_active = false
	spawn_timer.stop()
	boss_timer_label.visible = false
	boss_hint_label.visible  = false
	if is_instance_valid(portal_node):
		portal_node.queue_free()
	Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	(win_panel.get_node("VBox/ScoreLabel") as Label).text = "Score: %d\nWave: %d" % [score, wave]
	show_tutorial("VictoryScreen", func() -> void:
		get_tree().paused = true
		win_panel.visible = true
	)

func _trigger_time_up() -> void:
	if not boss_active:
		return
	boss_active = false
	game_active = false
	spawn_timer.stop()
	boss_timer_label.visible = false
	boss_hint_label.visible  = false
	if is_instance_valid(portal_node):
		portal_node.queue_free()
	_show_gameover()

# ─── Tutorial ─────────────────────────────────────────────────────────────────

func _create_tutorial_overlay() -> void:
	tutorial_canvas = CanvasLayer.new()
	tutorial_canvas.process_mode = Node.PROCESS_MODE_ALWAYS
	tutorial_canvas.visible = false
	add_child(tutorial_canvas)

	var bg := ColorRect.new()
	bg.color = Color(0.0, 0.0, 0.0, 0.78)
	bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	tutorial_canvas.add_child(bg)

	tutorial_image = TextureRect.new()
	tutorial_image.anchor_left   = 0.02
	tutorial_image.anchor_right  = 0.98
	tutorial_image.anchor_top    = 0.02
	tutorial_image.anchor_bottom = 0.91
	tutorial_image.stretch_mode  = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	tutorial_image.expand_mode   = TextureRect.EXPAND_IGNORE_SIZE
	tutorial_canvas.add_child(tutorial_image)

	tutorial_hint = Label.new()
	tutorial_hint.anchor_left  = 0.5
	tutorial_hint.anchor_right = 0.5
	tutorial_hint.anchor_top   = 0.93
	tutorial_hint.offset_left  = -280
	tutorial_hint.offset_right =  280
	tutorial_hint.text = "Нажмите ЛКМ чтобы продолжить"
	tutorial_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	tutorial_hint.add_theme_font_size_override("font_size", 22)
	tutorial_hint.add_theme_color_override("font_color", Color.WHITE)
	tutorial_hint.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.9))
	tutorial_hint.add_theme_constant_override("shadow_offset_x", 2)
	tutorial_hint.add_theme_constant_override("shadow_offset_y", 2)
	tutorial_hint.visible = false
	tutorial_canvas.add_child(tutorial_hint)

	var click_cap := Control.new()
	click_cap.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	click_cap.mouse_filter = Control.MOUSE_FILTER_STOP
	click_cap.process_mode = Node.PROCESS_MODE_ALWAYS
	click_cap.connect("gui_input", _on_tutorial_input)
	tutorial_canvas.add_child(click_cap)

func _on_tutorial_input(event: InputEvent) -> void:
	if not tutorial_hint_ready:
		return
	var mb := event as InputEventMouseButton
	if mb != null and mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
		_dismiss_tutorial()

func _set_enemies_paused(paused: bool) -> void:
	var mode := Node.PROCESS_MODE_DISABLED if paused else Node.PROCESS_MODE_PAUSABLE
	for e in get_tree().get_nodes_in_group("enemies"):
		(e as Node).process_mode = mode
	spawn_timer.paused = paused
	
func _set_player_paused(paused: bool) -> void:
	var mode := Node.PROCESS_MODE_DISABLED if paused else Node.PROCESS_MODE_PAUSABLE
	for e in get_tree().get_nodes_in_group("player"):
		(e as Node).process_mode = mode

const _REPEATABLE_TUTORIALS := ["VictoryScreen", "LoseGame"]

func show_tutorial(key: String, on_dismiss: Callable = Callable()) -> void:
	if shown_tutorials.get(key, false) and not key in _REPEATABLE_TUTORIALS:
		if on_dismiss.is_valid():
			on_dismiss.call()
		return
	shown_tutorials[key] = true
	var path := "res://assets/%s.jpeg" % key
	tutorial_image.texture = load(path) if ResourceLoader.exists(path) else null
	tutorial_on_dismiss = on_dismiss
	tutorial_hint_ready = false
	tutorial_hint.visible = false
	tutorial_canvas.visible = true
	tutorial_active = true
	_set_enemies_paused(true)
	_set_player_paused(true)
	await get_tree().create_timer(3.0).timeout
	tutorial_hint.visible = true
	tutorial_hint_ready = true

func _dismiss_tutorial() -> void:
	tutorial_canvas.visible = false
	tutorial_active = false
	_set_enemies_paused(false)
	_set_player_paused(false)
	if tutorial_on_dismiss.is_valid():
		tutorial_on_dismiss.call()

# ─── UI ───────────────────────────────────────────────────────────────────────

func _create_ui() -> void:
	canvas = CanvasLayer.new()
	canvas.process_mode = Node.PROCESS_MODE_ALWAYS
	add_child(canvas)

	_make_hud()
	_make_upgrade_panel()
	_make_gameover_panel()
	_make_boss_ui()
	_make_win_panel()

func _make_hud() -> void:
	score_label = _label(Vector2(16, 12), "Score: 0", 30)
	wave_label  = _label(Vector2(16, 50), "Wave: 1",  30)
	_make_recipe_panel()

func _make_recipe_panel() -> void:
	const INGREDIENT_NAMES := {
		"rock":       "Rock",
		"stick":      "Stick",
		"leather":    "Leather",
		"iron":       "Iron",
		"metal_plate":"Iron Plate",
		#"MetalPlate":"Armor boots"
		"essence":"Essence",
		"Enchanted_Table": "Ench Table",
		"EnchantedSphere": "Enchanted Boots"
	}
	const RESULT_NAMES := {
		"LeatherBoots":  "Leather Boots",
		"Boulder":       "Boulder",
		"StickArmor":    "Stick Armor",
		"LeatherArmor":  "Leather Armor",
		"PlateArmor":    "Plate Armor",
		"WoodenShield":  "Wooden Shield",
		"IronShield":    "Iron Shield",
		"MetalArmor":    "Metal Armor",
		"MetalPlate": "Armor boots"
	}

	const PAD_H    := 10
	const PAD_V    := 8
	const SEP      := 4
	const TITLE_H  := 22
	const ROW_H    := 20
	var n           := MergeRecipes._list.size()
	var content_h   := TITLE_H + SEP + n * ROW_H + (n - 1) * SEP
	var panel_h     := content_h + PAD_V * 2 + 10
	const PANEL_W  := 260

	var panel := Panel.new()
	var sb := StyleBoxFlat.new()
	sb.bg_color = Color(0.0, 0.0, 0.0, 0.55)
	sb.corner_radius_top_left     = 6
	sb.corner_radius_top_right    = 6
	sb.corner_radius_bottom_left  = 6
	sb.corner_radius_bottom_right = 6
	panel.add_theme_stylebox_override("panel", sb)
	panel.anchor_left   = 1.0
	panel.anchor_right  = 1.0
	panel.anchor_top    = 0.0
	panel.anchor_bottom = 0.0
	panel.offset_left   = -(PANEL_W + 10)
	panel.offset_right  = -10
	panel.offset_top    = 10
	panel.offset_bottom = 10 + panel_h
	canvas.add_child(panel)

	var margin := MarginContainer.new()
	margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	margin.add_theme_constant_override("margin_left",   PAD_H)
	margin.add_theme_constant_override("margin_right",  PAD_H)
	margin.add_theme_constant_override("margin_top",    PAD_V)
	margin.add_theme_constant_override("margin_bottom", PAD_V)
	panel.add_child(margin)

	var vbox := VBoxContainer.new()
	vbox.add_theme_constant_override("separation", SEP)
	margin.add_child(vbox)

	var title := Label.new()
	title.text = "Рецепты"
	title.add_theme_font_size_override("font_size", 17)
	title.add_theme_color_override("font_color", Color(1.0, 0.9, 0.5))
	title.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
	title.add_theme_constant_override("shadow_offset_x", 1)
	title.add_theme_constant_override("shadow_offset_y", 1)
	title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(title)

	for recipe in MergeRecipes._list:
		var a: String = INGREDIENT_NAMES.get(recipe["ingredients"][0], recipe["ingredients"][0])
		var b: String = INGREDIENT_NAMES.get(recipe["ingredients"][1], recipe["ingredients"][1])
		var scene_name: String = (recipe["result_scene"] as String).get_file().get_basename()
		var result: String = RESULT_NAMES.get(scene_name, scene_name)
		var row := Label.new()
		row.text = "%s + %s → %s" % [a, b, result]
		row.add_theme_font_size_override("font_size", 15)
		row.add_theme_color_override("font_color", Color.WHITE)
		row.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
		row.add_theme_constant_override("shadow_offset_x", 1)
		row.add_theme_constant_override("shadow_offset_y", 1)
		vbox.add_child(row)

	_label(Vector2(16, 92), "HP", 20)
	hp_bar_bg = _crect(Vector2(16, 118), Vector2(260, 20), Color(0.25, 0.04, 0.04))
	hp_bar    = _crect(Vector2(16, 118), Vector2(260, 20), Color(0.9, 0.15, 0.15))

	_label(Vector2(16, 146), "Next upgrade", 20)
	progress_bg  = _crect(Vector2(16, 172), Vector2(260, 14), Color(0.1, 0.1, 0.25))
	progress_bar = _crect(Vector2(16, 172), Vector2(0,   14), Color(0.4, 0.8, 1.0))

	tier_label = _label(Vector2(16, 194), "", 20)

	_make_equipment_slots()

func _make_upgrade_panel() -> void:
	upgrade_panel = Panel.new()
	upgrade_panel.process_mode = Node.PROCESS_MODE_ALWAYS
	upgrade_panel.visible = false
	canvas.add_child(upgrade_panel)

	# Center with explicit anchors (420x300)
	upgrade_panel.anchor_left   = 0.5
	upgrade_panel.anchor_right  = 0.5
	upgrade_panel.anchor_top    = 0.5
	upgrade_panel.anchor_bottom = 0.5
	upgrade_panel.offset_left   = -210.0
	upgrade_panel.offset_right  =  210.0
	upgrade_panel.offset_top    = -150.0
	upgrade_panel.offset_bottom =  150.0

	var vbox := VBoxContainer.new()
	vbox.name = "VBox"
	vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	vbox.add_theme_constant_override("separation", 12)
	vbox.alignment = BoxContainer.ALIGNMENT_CENTER
	upgrade_panel.add_child(vbox)

	var title := Label.new()
	title.text = "LEVEL UP! Choose an upgrade:"
	title.add_theme_font_size_override("font_size", 20)
	title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(title)

	for i in range(3):
		var btn := Button.new()
		btn.name = "Btn%d" % i
		btn.custom_minimum_size = Vector2(380, 60)
		btn.add_theme_font_size_override("font_size", 16)
		btn.process_mode = Node.PROCESS_MODE_ALWAYS
		vbox.add_child(btn)

func _make_gameover_panel() -> void:
	gameover_panel = Panel.new()
	gameover_panel.process_mode = Node.PROCESS_MODE_ALWAYS
	gameover_panel.visible = false
	canvas.add_child(gameover_panel)

	# Center with explicit anchors (360x240)
	gameover_panel.anchor_left   = 0.5
	gameover_panel.anchor_right  = 0.5
	gameover_panel.anchor_top    = 0.5
	gameover_panel.anchor_bottom = 0.5
	gameover_panel.offset_left   = -180.0
	gameover_panel.offset_right  =  180.0
	gameover_panel.offset_top    = -120.0
	gameover_panel.offset_bottom =  120.0

	var vbox := VBoxContainer.new()
	vbox.name = "VBox"
	vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	vbox.alignment = BoxContainer.ALIGNMENT_CENTER
	vbox.add_theme_constant_override("separation", 16)
	gameover_panel.add_child(vbox)

	var title := Label.new()
	title.text = "GAME OVER"
	title.add_theme_font_size_override("font_size", 32)
	title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(title)

	var score_lbl := Label.new()
	score_lbl.name = "ScoreLabel"
	score_lbl.add_theme_font_size_override("font_size", 20)
	score_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox.add_child(score_lbl)

	var restart_btn := Button.new()
	restart_btn.text = "Play Again"
	restart_btn.add_theme_font_size_override("font_size", 18)
	restart_btn.process_mode = Node.PROCESS_MODE_ALWAYS
	restart_btn.connect("pressed", _restart)
	vbox.add_child(restart_btn)

func _make_boss_ui() -> void:
	boss_timer_label = Label.new()
	boss_timer_label.anchor_left   = 0.5
	boss_timer_label.anchor_right  = 0.5
	boss_timer_label.anchor_top    = 0.0
	boss_timer_label.offset_left   = -80
	boss_timer_label.offset_right  =  80
	boss_timer_label.offset_top    = 8
	boss_timer_label.offset_bottom = 54
	boss_timer_label.text          = "1:30"
	boss_timer_label.add_theme_font_size_override("font_size", 38)
	boss_timer_label.add_theme_color_override("font_color", Color(0.8, 0.3, 1.0))
	boss_timer_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.9))
	boss_timer_label.add_theme_constant_override("shadow_offset_x", 2)
	boss_timer_label.add_theme_constant_override("shadow_offset_y", 2)
	boss_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	boss_timer_label.visible = false
	canvas.add_child(boss_timer_label)

	boss_hint_label = Label.new()
	boss_hint_label.anchor_left   = 0.5
	boss_hint_label.anchor_right  = 0.5
	boss_hint_label.anchor_top    = 0.0
	boss_hint_label.offset_left   = -220
	boss_hint_label.offset_right  =  220
	boss_hint_label.offset_top    = 54
	boss_hint_label.offset_bottom = 80
	boss_hint_label.text          = "Push the Ogre into the Portal!"
	boss_hint_label.add_theme_font_size_override("font_size", 17)
	boss_hint_label.add_theme_color_override("font_color", Color(0.85, 0.7, 1.0))
	boss_hint_label.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
	boss_hint_label.add_theme_constant_override("shadow_offset_x", 1)
	boss_hint_label.add_theme_constant_override("shadow_offset_y", 1)
	boss_hint_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	boss_hint_label.visible = false
	canvas.add_child(boss_hint_label)

func _make_win_panel() -> void:
	win_panel = Panel.new()
	win_panel.process_mode = Node.PROCESS_MODE_ALWAYS
	win_panel.visible = false
	canvas.add_child(win_panel)

	win_panel.anchor_left   = 0.5
	win_panel.anchor_right  = 0.5
	win_panel.anchor_top    = 0.5
	win_panel.anchor_bottom = 0.5
	win_panel.offset_left   = -200.0
	win_panel.offset_right  =  200.0
	win_panel.offset_top    = -140.0
	win_panel.offset_bottom =  140.0

	var vbox2 := VBoxContainer.new()
	vbox2.name = "VBox"
	vbox2.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	vbox2.alignment = BoxContainer.ALIGNMENT_CENTER
	vbox2.add_theme_constant_override("separation", 16)
	win_panel.add_child(vbox2)

	var title2 := Label.new()
	title2.text = "VICTORY!"
	title2.add_theme_font_size_override("font_size", 42)
	title2.add_theme_color_override("font_color", Color(1.0, 0.85, 0.1))
	title2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox2.add_child(title2)

	var score_lbl2 := Label.new()
	score_lbl2.name = "ScoreLabel"
	score_lbl2.add_theme_font_size_override("font_size", 20)
	score_lbl2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	vbox2.add_child(score_lbl2)

	var btn2 := Button.new()
	btn2.text = "Play Again"
	btn2.add_theme_font_size_override("font_size", 18)
	btn2.process_mode = Node.PROCESS_MODE_ALWAYS
	btn2.connect("pressed", _restart)
	vbox2.add_child(btn2)

# ─── Equipment slots ──────────────────────────────────────────────────────────

const _EQUIP_SLOT_SIZE  := 64
const _EQUIP_SLOT_GAP   := 10
const _EQUIP_SLOT_PAD   := 3
const _EQUIP_START_X    := 16
const _EQUIP_START_Y    := 228
const _EQUIP_EMPTY_COL  := Color(0.10, 0.10, 0.16)
const _EQUIP_BORDER_COL := Color(0.30, 0.30, 0.42)

# [slot_index][tier] = [fill_color, label_text]
const _EQUIP_DATA := [
	[  # shield
		[Color(0.10, 0.10, 0.16), ""],
		[Color(0.55, 0.38, 0.18), "Wood\nShield"],
		[Color(0.55, 0.58, 0.62), "Iron\nShield"],
	],
	[  # armor (toughness_tier)
		[Color(0.10, 0.10, 0.16), ""],
		[Color(0.76, 0.47, 0.18), "Leather\nArmor"],
		[Color(0.58, 0.68, 1.00), "Metal\nArmor"],
	],
	[  # boots (kick_tier)
		[Color(0.10, 0.10, 0.16), ""],
		[Color(0.76, 0.47, 0.18), "Leather\nBoots"],
		[Color(0.58, 0.68, 1.00), "Plate\nBoots"],
		[Color(0.75, 0.18, 1.00), "Magic\nBoots"],
	],
]

func _make_equipment_slots() -> void:
	var titles := ["Shield", "Armor", "Boots"]
	for i in range(3):
		var x := _EQUIP_START_X + i * (_EQUIP_SLOT_SIZE + _EQUIP_SLOT_GAP)
		var y := _EQUIP_START_Y

		var title := _label(Vector2(x, y), titles[i], 16)
		title.size.x = _EQUIP_SLOT_SIZE
		title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER

		var border := ColorRect.new()
		border.position = Vector2(x, y + 22)
		border.size = Vector2(_EQUIP_SLOT_SIZE, _EQUIP_SLOT_SIZE)
		border.color = _EQUIP_BORDER_COL
		canvas.add_child(border)

		var fill := ColorRect.new()
		fill.position = Vector2(x + _EQUIP_SLOT_PAD, y + 22 + _EQUIP_SLOT_PAD)
		fill.size = Vector2(_EQUIP_SLOT_SIZE - _EQUIP_SLOT_PAD * 2, _EQUIP_SLOT_SIZE - _EQUIP_SLOT_PAD * 2)
		fill.color = _EQUIP_EMPTY_COL
		canvas.add_child(fill)
		equip_fills.append(fill)

		var lbl := Label.new()
		lbl.position = fill.position
		lbl.size = fill.size
		lbl.text = ""
		lbl.add_theme_font_size_override("font_size", 14)
		lbl.add_theme_color_override("font_color", Color.WHITE)
		lbl.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.85))
		lbl.add_theme_constant_override("shadow_offset_x", 1)
		lbl.add_theme_constant_override("shadow_offset_y", 1)
		lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
		lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
		canvas.add_child(lbl)
		equip_labels.append(lbl)

func _update_equipment_slots() -> void:
	if not is_instance_valid(player):
		return
	var tiers := [
		player.get("shield_tier") as int,
		player.get("toughness_tier") as int,
		player.get("kick_tier") as int,
	]
	for i in range(3):
		var tier: int = tiers[i]
		if tier == _equip_prev_tiers[i]:
			continue
		_equip_prev_tiers[i] = tier
		var data: Array = _EQUIP_DATA[i]
		var entry: Array = data[clampi(tier, 0, data.size() - 1)]
		equip_fills[i].color = entry[0] as Color
		equip_labels[i].text = entry[1] as String
		if tier > 0:
			var tw := create_tween()
			tw.tween_property(equip_fills[i], "color", Color.WHITE, 0.06)
			tw.tween_property(equip_fills[i], "color", entry[0] as Color, 0.22)

# ─── UI helpers ───────────────────────────────────────────────────────────────

func _label(pos: Vector2, text: String, size: int) -> Label:
	var lbl := Label.new()
	lbl.position = pos
	lbl.text = text
	lbl.add_theme_font_size_override("font_size", size)
	lbl.add_theme_color_override("font_color", Color.WHITE)
	lbl.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.8))
	lbl.add_theme_constant_override("shadow_offset_x", 2)
	lbl.add_theme_constant_override("shadow_offset_y", 2)
	canvas.add_child(lbl)
	return lbl

func _crect(pos: Vector2, sz: Vector2, col: Color) -> ColorRect:
	var r := ColorRect.new()
	r.position = pos
	r.size = sz
	r.color = col
	canvas.add_child(r)
	return r

func _update_labels() -> void:
	score_label.text = "Score: %d" % score
	wave_label.text  = "Wave: %d" % wave

func _update_progress() -> void:
	var t := float(kills) / float(kills_for_next)
	progress_bar.size.x = 200.0 * t

func _update_tier_label() -> void:
	if not is_instance_valid(player):
		return
	var kt: int = player.get("kick_tier")
	var tt: int = player.get("toughness_tier")
	var st: int = player.get("shield_tier")
	var shield_str := "-" if st == 0 else str(st)
	tier_label.text = "Kick: %d  Tough: %d  Shield: %s" % [kt, tt, shield_str]
	_update_equipment_slots()
