extends CanvasLayer

const _GOLD  := Color(1.00, 0.78, 0.22)
const _CREAM := Color(0.93, 0.88, 0.72)
const _MUTED := Color(0.66, 0.60, 0.46)

# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
# Order matches _ABILITY_ICONS: kick, dash, slam, roll.
const _ABILITY_ACCENT: Array[Color] = [
	Color(1.00, 0.62, 0.16),   # Kick — ember orange
	Color(0.45, 0.75, 1.00),   # Dash — sky blue
	Color(0.98, 0.34, 0.20),   # Slam — crimson
	Color(1.00, 0.82, 0.32),   # Roll — gold
]

const _ABILITY_ICONS: Array[String] = [
	"res://HUD/KICK.png",
	"res://HUD/DASH.png",
	"res://HUD/SLAM.png",
	"res://HUD/ROLL.png",
]
const _ABILITY_ACTIONS: Array[StringName] = [
	&"ability_kick", &"ability_dash", &"ability_slam", &"ability_roll",
]
# Fallback label shown in a slot whose icon file fails to load.
const _ABILITY_NAMES: Array[String] = ["Kick", "Dash", "Slam", "Roll"]

var _controls_visible: bool = false
var _controls_panel: PanelContainer
var _toggle_btn: Button
var _tab_hint: Label
var _spawner: Node
var _player: Node = null
var _died_connected: bool = false
var _cd_overlays: Array[ColorRect] = []
var _cd_labels: Array[Label] = []
var _hp_label: Label
var _hp_segments: HBoxContainer
var _hp_seg_fills: Array[ColorRect] = []
var _bottom_col: VBoxContainer
var _ability_bar: HBoxContainer
var _ability_panels: Array[PanelContainer] = []
var _bull_bg: TextureRect

# Red damage vignette — a full-screen shader ColorRect flashed on every hit.
const _VIGNETTE_SHADER := "res://damage_vignette.gdshader"
var _vignette: ColorRect
var _vignette_mat: ShaderMaterial

# Bull-head backdrop for the gameplay HUD. The image is a wide horned silhouette; its
# solid "face" occupies the centred middle band, with horns flaring above. We size the
# image so that face band wraps the ability+HP cluster (horns crowning it above).
const _BULL_BG := "res://Assets/bull_hud_bg.png"
const _BULL_FACE_W: float  = 0.529    # face width as a fraction of image width
const _BULL_ASPECT: float  = 0.2741   # image height / width (267 / 974)
const _BULL_FACE_PAD: float = 1.15    # breathing room around the cluster within the face

# Win/lose screen. _game_over latches so the result is shown once; _result_win
# records which way it went (read by the gameplay test).
const _WIN_VIDEO  := "res://videos/bull_win.ogv"       # the bull triumphs
const _LOSS_VIDEO := "res://videos/matador_win.ogv"    # the matador triumphs

# Beat between the run ending and the outcome screen. A loss is a short freeze on the
# dead bull; a win gives the player a longer stretch to strut a victory lap around the
# arena before the screen (and the pause) takes over.
const _LOSS_DELAY := 2.0
const _WIN_DELAY  := 3.0

var _game_over: bool = false
var _result_win: bool = false
var _over_root: Control
var _over_video: VideoStreamPlayer
var _over_label: Label


func _ready() -> void:
	layer = 11
	process_mode = Node.PROCESS_MODE_ALWAYS
	_build_controls_panel()
	_build_tab_hint()
	_build_bottom_hud()
	_build_bull_hud_bg()
	_build_ability_bar()
	_build_health()
	_build_version_label()
	_build_damage_vignette()
	_build_game_over()
	_build_touch_controls()
	_update_control_hints()
	_find_spawner.call_deferred()


# On-screen joystick + ability buttons for touch devices. Self-gating: the overlay
# only shows itself when a touchscreen is present (or DP force_touch_controls is on),
# so this is a no-op on desktop.
func _build_touch_controls() -> void:
	add_child((load("res://touch_controls.gd") as Script).new())


# Bottom-centre column that groups the gameplay HUD (abilities on top, bull health
# directly beneath) so both read as one cluster instead of scattered corners.
func _build_bottom_hud() -> void:
	_bottom_col = VBoxContainer.new()
	_bottom_col.add_theme_constant_override("separation", 8)
	_bottom_col.alignment       = BoxContainer.ALIGNMENT_END
	# Collapsed horizontal anchors so the column shrinks to its content and stays
	# bottom-centred; the bull backdrop is sized against this content rect.
	_bottom_col.anchor_left     = 0.5
	_bottom_col.anchor_right    = 0.5
	_bottom_col.anchor_top      = 1.0
	_bottom_col.anchor_bottom   = 1.0
	_bottom_col.grow_horizontal = Control.GROW_DIRECTION_BOTH
	_bottom_col.grow_vertical   = Control.GROW_DIRECTION_BEGIN
	_bottom_col.offset_bottom   = -16.0
	_bottom_col.mouse_filter    = Control.MOUSE_FILTER_IGNORE
	add_child(_bottom_col)


# Bull-head plate that sits behind the ability + health cluster. It's a sibling of the
# cluster (not a parent) so it never constrains the cluster's layout; we size and place
# it against the cluster's rect in _position_bull_bg.
func _build_bull_hud_bg() -> void:
	_bull_bg = TextureRect.new()
	if ResourceLoader.exists(_BULL_BG):
		_bull_bg.texture = load(_BULL_BG)
	# We size the rect ourselves to the image's aspect, so STRETCH_SCALE fills it
	# exactly with no letterboxing.
	_bull_bg.expand_mode  = TextureRect.EXPAND_IGNORE_SIZE
	_bull_bg.stretch_mode = TextureRect.STRETCH_SCALE
	_bull_bg.modulate.a   = 0.95
	_bull_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
	add_child(_bull_bg)
	# Draw the plate behind the cluster.
	move_child(_bull_bg, _bottom_col.get_index())

	_bottom_col.resized.connect(_position_bull_bg)
	get_viewport().size_changed.connect(_position_bull_bg)
	_position_bull_bg.call_deferred()


# Wrap the bull's solid "face" band around the current cluster rect: scale the image up
# so its face spans the cluster (plus padding), centre it horizontally on the cluster,
# and pin its bottom edge to the bottom of the screen so the plate is always seated flush
# against the screen edge on any resolution (horns crowning the cluster above).
func _position_bull_bg() -> void:
	if _bull_bg == null or _bottom_col == null:
		return
	var r := _bottom_col.get_global_rect()
	if r.size.x <= 1.0:
		return
	var vp_h := get_viewport().get_visible_rect().size.y
	var bw := (r.size.x * _BULL_FACE_PAD) / _BULL_FACE_W
	var bh := bw * _BULL_ASPECT
	_bull_bg.size = Vector2(bw, bh)
	_bull_bg.position = Vector2(
		r.position.x + r.size.x * 0.5 - bw * 0.5,
		vp_h - bh)


# Unobtrusive early-build tag in the bottom-right corner.
func _build_version_label() -> void:
	var lbl := Label.new()
	lbl.text = GameVersion.full()
	lbl.add_theme_color_override("font_color", _MUTED)
	lbl.add_theme_font_size_override("font_size", 12)
	lbl.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
	lbl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
	lbl.grow_vertical = Control.GROW_DIRECTION_BEGIN
	lbl.offset_right = -14.0
	lbl.offset_bottom = -10.0
	lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
	add_child(lbl)


func _process(_delta: float) -> void:
	_update_control_hints()
	if _player == null:
		var players := get_tree().get_nodes_in_group(&"player")
		if not players.is_empty():
			_player = players[0]
		return
	if not _died_connected:
		_died_connected = true
		_player.died.connect(_on_player_died)
		_player.health_changed.connect(_on_health_changed)
		_player.hit_taken.connect(_on_hit_taken)
		_on_health_changed(_player.call(&"get_hp"), _player.call(&"get_max_hp"))
	for i: int in _cd_overlays.size():
		var fraction: float = _player.call(&"ability_cooldown_fraction", i)
		var ov := _cd_overlays[i]
		ov.anchor_top    = 1.0 - fraction
		ov.anchor_bottom = 1.0
		var remaining: float = _player.call(&"ability_cooldown_remaining", i)
		var lbl := _cd_labels[i]
		if remaining > 0.0:
			lbl.text    = "%.1fs" % remaining
			lbl.visible = true
		else:
			lbl.visible = false


func _find_spawner() -> void:
	var spawners := get_tree().get_nodes_in_group(&"matador_spawn")
	if spawners.is_empty():
		return
	_spawner = spawners[0]
	_spawner.all_defeated.connect(_on_all_defeated)


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and not event.echo:
		if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
			_restart()
		elif event.physical_keycode == KEY_TAB and not _game_over:
			_toggle_controls()
		elif event.physical_keycode == KEY_ESCAPE:
			_return_to_menu()


func _return_to_menu() -> void:
	get_tree().paused = false
	Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	get_tree().change_scene_to_file("res://MainMenu.tscn")


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


func _toggle_controls() -> void:
	_controls_visible = not _controls_visible
	_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
	_update_control_hints()


# The Tab hint and the keyboard controls plaque only make sense with a keyboard, so
# they stand down on touch devices — where touch_controls.gd shows the on-screen
# joystick and ability buttons instead. Shares Controls.use_touch_ui() with that
# overlay so exactly one control scheme shows on any given device.
func _update_control_hints() -> void:
	if _game_over or Controls.use_touch_ui():
		_controls_panel.visible = false
		_tab_hint.visible = false
	else:
		_controls_panel.visible = _controls_visible
		_tab_hint.visible = not _controls_visible


# ── Damage vignette ───────────────────────────────────────────────────────────
# A full-screen ColorRect driven by damage_vignette.gdshader: transparent centre, red
# toward the edges, alpha ("flash") tweened up on a hit and eased back to nothing. Built
# before the game-over layer so the outcome screen always draws over it.

func _build_damage_vignette() -> void:
	_vignette = ColorRect.new()
	_vignette.set_anchors_preset(Control.PRESET_FULL_RECT)
	_vignette.color = Color.WHITE  # the shader writes COLOR outright; this is just a base
	_vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE
	if ResourceLoader.exists(_VIGNETTE_SHADER):
		_vignette_mat = ShaderMaterial.new()
		_vignette_mat.shader = load(_VIGNETTE_SHADER)
		_vignette_mat.set_shader_parameter("flash", 0.0)
		_vignette.material = _vignette_mat
	add_child(_vignette)


func _on_hit_taken(_cause: String) -> void:
	if _vignette_mat == null:
		return
	var set_flash := func(v: float) -> void:
		_vignette_mat.set_shader_parameter("flash", v)
	create_tween().tween_method(set_flash, 0.9, 0.0, 0.5)\
		.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)


# ── Win / lose ──────────────────────────────────────────────────────────────
# The bull takes one clean blade and it's over; clear the last matador and it's a
# victory. Both routes end here — the arena freezes and the result is proclaimed.

func _on_player_died(cause: String = "") -> void:
	_show_game_over(false, cause)


func _on_all_defeated() -> void:
	_show_game_over(true)


func _show_game_over(win: bool, _cause: String = "") -> void:
	if _game_over:
		return
	_game_over = true
	_result_win = win

	if _controls_panel:
		_controls_panel.visible = false
	if _tab_hint:
		_tab_hint.visible = false

	await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout

	_over_label.text = "Bull Won!" if _result_win else "Matadors Won!"

	var path := _WIN_VIDEO if _result_win else _LOSS_VIDEO
	if ResourceLoader.exists(path):
		_over_video.stream = load(path)
		_over_video.play()

	_over_root.visible = true
	_over_root.modulate.a = 0.0
	Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
	get_tree().paused = true

	create_tween().tween_property(_over_root, "modulate:a", 1.0, 0.5)


func _build_game_over() -> void:
	_over_root = Control.new()
	_over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
	_over_root.process_mode = Node.PROCESS_MODE_ALWAYS
	_over_root.visible = false
	add_child(_over_root)

	# Full-screen outcome clip (bull win / matador win), looping.
	# AspectRatioContainer keeps the 16:9 video's ratio and crops overflow (COVER),
	# so it fills any screen shape without stretching or leaving dead bars.
	var arc := AspectRatioContainer.new()
	arc.set_anchors_preset(Control.PRESET_FULL_RECT)
	arc.ratio = 16.0 / 9.0
	arc.stretch_mode = AspectRatioContainer.STRETCH_COVER
	arc.alignment_horizontal = AspectRatioContainer.ALIGNMENT_CENTER
	arc.alignment_vertical = AspectRatioContainer.ALIGNMENT_CENTER
	arc.process_mode = Node.PROCESS_MODE_ALWAYS
	arc.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_over_root.add_child(arc)

	_over_video = VideoStreamPlayer.new()
	_over_video.set_anchors_preset(Control.PRESET_FULL_RECT)
	_over_video.expand = true
	_over_video.process_mode = Node.PROCESS_MODE_ALWAYS
	_over_video.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_over_video.finished.connect(_over_video.play)  # loop the outcome clip
	arc.add_child(_over_video)

	# Outcome text centred over the video.
	_over_label = Label.new()
	_over_label.set_anchors_preset(Control.PRESET_CENTER_TOP)
	_over_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
	_over_label.grow_vertical   = Control.GROW_DIRECTION_END
	_over_label.offset_top      = 80.0
	_over_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	_over_label.add_theme_font_override("font", UiFonts.title())
	_over_label.add_theme_font_size_override("font_size", 64)
	_over_label.add_theme_color_override("font_color",         _GOLD)
	_over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
	_over_label.add_theme_constant_override("outline_size",    6)
	_over_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_over_root.add_child(_over_label)

	# Buttons overlaid at the bottom of the clip.
	var row := HBoxContainer.new()
	row.alignment = BoxContainer.ALIGNMENT_CENTER
	row.add_theme_constant_override("separation", 16)
	row.anchor_left   = 0.5
	row.anchor_right  = 0.5
	row.anchor_top    = 1.0
	row.anchor_bottom = 1.0
	row.grow_horizontal = Control.GROW_DIRECTION_BOTH
	row.grow_vertical   = Control.GROW_DIRECTION_BEGIN
	row.offset_bottom   = -48.0
	_over_root.add_child(row)

	var again := _make_button("Go Again  (Enter)")
	again.pressed.connect(_restart)
	row.add_child(again)

	var menu := _make_button("Main Menu")
	menu.pressed.connect(_return_to_menu)
	row.add_child(menu)

# ── Bull health ───────────────────────────────────────────────────────────────
# A row of gold-socketed pips beneath the ability bar — one pip per HP, so raising or
# lowering max HP visibly grows or shrinks the row. Lit pips run red and shade toward
# crimson as the bull is worn down; spent pips read as empty dark sockets.

const _HP_FULL:   Color = Color(0.82, 0.16, 0.12)
const _HP_EMPTY:  Color = Color(0.35, 0.05, 0.05)
const _HP_SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92)

const _HP_PIP_W: float = 22.0
const _HP_PIP_H: float = 14.0


func _build_health() -> void:
	var panel := PanelContainer.new()
	panel.add_theme_stylebox_override(
		"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 12.0, 6.0))
	panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_bottom_col.add_child(panel)

	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 8)
	panel.add_child(row)

	_hp_label = Label.new()
	_hp_label.text = "BULL"
	_hp_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	_hp_label.add_theme_font_override("font", UiFonts.body())
	_hp_label.add_theme_color_override("font_color",         _GOLD)
	_hp_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
	_hp_label.add_theme_constant_override("outline_size",    3)
	_hp_label.add_theme_font_size_override("font_size", 14)
	row.add_child(_hp_label)

	_hp_segments = HBoxContainer.new()
	_hp_segments.add_theme_constant_override("separation", 4)
	_hp_segments.size_flags_vertical = Control.SIZE_SHRINK_CENTER
	_hp_segments.mouse_filter = Control.MOUSE_FILTER_IGNORE
	row.add_child(_hp_segments)


# The gold-underlined socket behind each pip — shared, non-mutating, so one instance
# is safe across every pip.
func _hp_socket_style() -> StyleBoxFlat:
	var s := StyleBoxFlat.new()
	s.bg_color = _HP_SOCKET
	s.set_corner_radius_all(3)
	s.corner_detail = 4
	s.border_width_bottom = 2
	s.border_color = Color(UiTheme.GOLD.r, UiTheme.GOLD.g, UiTheme.GOLD.b, 0.45)
	s.content_margin_left   = 2.0
	s.content_margin_right  = 2.0
	s.content_margin_top    = 2.0
	s.content_margin_bottom = 2.0
	return s


func _rebuild_hp_pips(max_hp: int) -> void:
	for child in _hp_segments.get_children():
		child.queue_free()
	_hp_seg_fills.clear()
	var socket_style := _hp_socket_style()
	for _i: int in maxi(max_hp, 0):
		var socket := PanelContainer.new()
		socket.custom_minimum_size = Vector2(_HP_PIP_W, _HP_PIP_H)
		socket.clip_contents = true
		socket.add_theme_stylebox_override("panel", socket_style)
		socket.mouse_filter = Control.MOUSE_FILTER_IGNORE
		_hp_segments.add_child(socket)

		var fill := ColorRect.new()
		fill.color = _HP_FULL
		fill.mouse_filter = Control.MOUSE_FILTER_IGNORE
		socket.add_child(fill)
		_hp_seg_fills.append(fill)


func _on_health_changed(current: int, max_hp: int) -> void:
	if _hp_segments == null:
		return
	if _hp_seg_fills.size() != max_hp:
		_rebuild_hp_pips(max_hp)
	var cur := maxi(current, 0)
	var frac := 0.0 if max_hp <= 0 else clampf(float(cur) / float(max_hp), 0.0, 1.0)
	var lit := _HP_FULL.lerp(_HP_EMPTY, 1.0 - frac)
	for i: int in _hp_seg_fills.size():
		var fill := _hp_seg_fills[i]
		fill.visible = i < cur
		fill.color   = lit


const _SLOT_SEPARATION: int = 10


func _build_ability_bar() -> void:
	_ability_bar = HBoxContainer.new()
	_ability_bar.add_theme_constant_override("separation", _SLOT_SEPARATION)
	_ability_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	_ability_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
	_bottom_col.add_child(_ability_bar)
	for i: int in 4:
		_ability_bar.add_child(_build_ability_slot(i))
	_layout_ability_bar()
	get_viewport().size_changed.connect(_layout_ability_bar)


# Size the ability slots as a fraction of the viewport (clamped) so the bar scales
# with the window; the bottom column keeps the bar horizontally centred.
func _layout_ability_bar() -> void:
	var vp := get_viewport().get_visible_rect().size
	var slot := clampf(minf(vp.x * 0.055, vp.y * 0.11), 56.0, 104.0)
	for panel: PanelContainer in _ability_panels:
		panel.custom_minimum_size = Vector2(slot, slot)


func _build_ability_slot(idx: int) -> Control:
	var vbox := VBoxContainer.new()
	vbox.add_theme_constant_override("separation", 3)

	var panel := PanelContainer.new()
	panel.custom_minimum_size = Vector2(72.0, 72.0)
	panel.add_theme_stylebox_override(
		"panel", UiTheme.plaque(UiTheme.LEATHER, _ABILITY_ACCENT[idx], 4, 5.0, 5.0))
	vbox.add_child(panel)
	_ability_panels.append(panel)

	var stack := Control.new()
	stack.set_anchors_preset(Control.PRESET_FULL_RECT)
	panel.add_child(stack)

	if ResourceLoader.exists(_ABILITY_ICONS[idx]):
		var icon := TextureRect.new()
		icon.texture      = load(_ABILITY_ICONS[idx])
		# Ignore the texture's native size so a large source PNG doesn't blow the
		# slot's minimum size out; scale-to-fit the box while keeping aspect.
		icon.expand_mode  = TextureRect.EXPAND_IGNORE_SIZE
		icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
		icon.set_anchors_preset(Control.PRESET_FULL_RECT)
		icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
		stack.add_child(icon)
	else:
		var name_lbl := Label.new()
		name_lbl.text = _ABILITY_NAMES[idx]
		name_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
		name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
		name_lbl.vertical_alignment   = VERTICAL_ALIGNMENT_CENTER
		name_lbl.mouse_filter         = Control.MOUSE_FILTER_IGNORE
		name_lbl.add_theme_color_override("font_color", _GOLD)
		name_lbl.add_theme_font_size_override("font_size", 16)
		stack.add_child(name_lbl)

	var cd_overlay := ColorRect.new()
	cd_overlay.color         = Color(0.0, 0.0, 0.0, 0.70)
	cd_overlay.anchor_left   = 0.0
	cd_overlay.anchor_right  = 1.0
	cd_overlay.anchor_top    = 1.0
	cd_overlay.anchor_bottom = 1.0
	cd_overlay.mouse_filter  = Control.MOUSE_FILTER_IGNORE
	stack.add_child(cd_overlay)
	_cd_overlays.append(cd_overlay)

	var cd_lbl := Label.new()
	cd_lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
	cd_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	cd_lbl.vertical_alignment   = VERTICAL_ALIGNMENT_CENTER
	cd_lbl.mouse_filter         = Control.MOUSE_FILTER_IGNORE
	cd_lbl.visible              = false
	cd_lbl.add_theme_color_override("font_color", Color.WHITE)
	cd_lbl.add_theme_font_size_override("font_size", 18)
	stack.add_child(cd_lbl)
	_cd_labels.append(cd_lbl)

	# Keycap tucked directly beneath the tile — a solid dark plate wearing the
	# ability's accent underline, so it reads as attached to its box without ever
	# covering the artwork. Centred under the tile and tightly spaced.
	var keycap := PanelContainer.new()
	keycap.add_theme_stylebox_override("panel", _keycap_style(idx))
	keycap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	keycap.mouse_filter = Control.MOUSE_FILTER_IGNORE
	vbox.add_child(keycap)

	var key_lbl := Label.new()
	key_lbl.text = Controls.get_key_label(_ABILITY_ACTIONS[idx])
	key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	key_lbl.vertical_alignment   = VERTICAL_ALIGNMENT_CENTER
	key_lbl.mouse_filter         = Control.MOUSE_FILTER_IGNORE
	key_lbl.add_theme_font_override("font", UiFonts.body_bold())
	key_lbl.add_theme_color_override("font_color",         _CREAM)
	key_lbl.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
	key_lbl.add_theme_constant_override("outline_size",    3)
	key_lbl.add_theme_font_size_override("font_size",  14)
	keycap.add_child(key_lbl)

	return vbox


# Solid keycap plate for an ability bind: near-black fill with the ability's accent
# as a chunky top edge, so the accent bridges up to the tile it sits under (reads as
# attached) while the key stays on a high-contrast background against busy scenes.
func _keycap_style(idx: int) -> StyleBoxFlat:
	var s := StyleBoxFlat.new()
	s.bg_color = Color(0.04, 0.03, 0.02, 0.94)
	s.set_corner_radius_all(4)
	s.corner_detail = 4
	s.set_border_width_all(1)
	s.border_width_top = 3
	s.border_color = _ABILITY_ACCENT[idx]
	s.content_margin_left   = 7.0
	s.content_margin_right   = 7.0
	s.content_margin_top    = 2.0
	s.content_margin_bottom = 1.0
	return s


func _build_tab_hint() -> void:
	_tab_hint = Label.new()
	_tab_hint.text = "Press 'Tab' to see controls"
	_tab_hint.anchor_left   = 0.0
	_tab_hint.anchor_top    = 1.0
	_tab_hint.anchor_right  = 0.0
	_tab_hint.anchor_bottom = 1.0
	_tab_hint.grow_vertical = Control.GROW_DIRECTION_BEGIN
	_tab_hint.offset_left   = 20.0
	_tab_hint.offset_bottom = -20.0
	_tab_hint.add_theme_color_override("font_color", _MUTED)
	_tab_hint.add_theme_font_size_override("font_size", 12)
	add_child(_tab_hint)


func _build_controls_panel() -> void:
	_controls_panel = PanelContainer.new()
	_controls_panel.add_theme_stylebox_override(
		"panel", UiTheme.plaque(UiTheme.LEATHER, UiTheme.BORDER, 4, 16.0, 12.0))

	_controls_panel.anchor_left   = 0.0
	_controls_panel.anchor_top    = 1.0
	_controls_panel.anchor_right  = 0.0
	_controls_panel.anchor_bottom = 1.0
	_controls_panel.grow_vertical = Control.GROW_DIRECTION_BEGIN
	_controls_panel.offset_left   = 20.0
	_controls_panel.offset_bottom = -20.0
	_controls_panel.visible = false
	add_child(_controls_panel)

	var vbox := VBoxContainer.new()
	vbox.add_theme_constant_override("separation", 5)
	_controls_panel.add_child(vbox)

	var move_keys := "%s %s %s %s" % [
		Controls.get_key_label(&"move_forward"),
		Controls.get_key_label(&"move_left"),
		Controls.get_key_label(&"move_back"),
		Controls.get_key_label(&"move_right"),
	]
	_row(vbox, move_keys,       "Move",                 _CREAM)
	_row(vbox, Controls.get_key_label(&"ability_kick"), "Kick",   _CREAM)
	_row(vbox, Controls.get_key_label(&"ability_dash"), "Dash",   _CREAM)
	_row(vbox, Controls.get_key_label(&"ability_slam"), "Slam",   _CREAM)
	_row(vbox, Controls.get_key_label(&"ability_roll"), "Roll (Powerball: ram the matador!)", _CREAM)
	_row(vbox, "Scroll",        "Zoom",                 _CREAM)
	_row(vbox, "Tab",           "Toggle controls",       _MUTED)
	_row(vbox, "Esc",           "Back to menu",          _MUTED)

	var sep := HSeparator.new()
	sep.add_theme_stylebox_override("separator", UiTheme.rule())
	vbox.add_child(sep)

	_toggle_btn = _make_button("Show controls")
	_toggle_btn.pressed.connect(_toggle_controls)
	vbox.add_child(_toggle_btn)


func _make_button(label_text: String) -> Button:
	var btn := Button.new()
	btn.text = label_text
	btn.process_mode = Node.PROCESS_MODE_ALWAYS

	var normal := UiTheme.plaque(Color(0.10, 0.07, 0.03, 0.90), _GOLD,  3, 18.0, 9.0)
	var hover  := UiTheme.plaque(Color(0.16, 0.10, 0.04, 0.96), _CREAM, 3, 18.0, 9.0)

	btn.add_theme_stylebox_override("normal", normal)
	btn.add_theme_stylebox_override("hover", hover)
	btn.add_theme_stylebox_override("pressed", hover)
	btn.add_theme_stylebox_override("focus", hover)
	btn.add_theme_font_override("font", UiFonts.body())
	btn.add_theme_color_override("font_color", _GOLD)
	btn.add_theme_color_override("font_hover_color", _CREAM)
	btn.add_theme_font_size_override("font_size", 18)

	return btn


func _row(parent: VBoxContainer, key: String, desc: String, desc_color: Color) -> void:
	var row := HBoxContainer.new()
	row.add_theme_constant_override("separation", 10)
	parent.add_child(row)

	var badge := PanelContainer.new()
	badge.add_theme_stylebox_override("panel", UiTheme.badge())
	badge.custom_minimum_size = Vector2(100.0, 0.0)
	row.add_child(badge)

	var key_lbl := Label.new()
	key_lbl.text = key
	key_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	key_lbl.vertical_alignment   = VERTICAL_ALIGNMENT_CENTER
	key_lbl.add_theme_color_override("font_color", _GOLD)
	key_lbl.add_theme_font_size_override("font_size", 12)
	badge.add_child(key_lbl)

	var desc_lbl := Label.new()
	desc_lbl.text = desc
	desc_lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
	desc_lbl.add_theme_color_override("font_color", desc_color)
	desc_lbl.add_theme_font_size_override("font_size", 12)
	row.add_child(desc_lbl)
