ajhahn.de
← Theria
GDScript 95 lines
class_name ErrorOverlay
extends Control
## The full-screen error screen, shown when a match fails in a way that would otherwise grey the
## screen or quit without a word — a host that cannot open its port, a join that reaches no server,
## a refused or dropped connection. It names the failure, shows its code (so a bug report can quote
## it), and offers the player a way out: back to the connect menu, or quit. Pure presentation — it
## owns no networking; `main.gd` shows it on a failure and acts on its two signals.
##
## Unlike the death screen (a dim the live world plays on behind), this is an OPAQUE cover: the
## match behind it is broken or gone, so nothing should show through or take a click meant for the
## buttons. A headless run never builds it — there is no screen, and a failed smoke just exits.

## The player chose the connect menu — the driver tears the failed match down and reopens it.
signal menu_requested
## The player chose to quit the game.
signal quit_requested

const TITLE_COLOR := Color(0.90, 0.40, 0.32)  # a warm alarm red, distinct from the amber accent
const TITLE_FONT_SIZE := 56
const CODE_FONT_SIZE := 28
const DETAIL_FONT_SIZE := 22
const DETAIL_MAX_WIDTH := 760.0

var _title: Label
var _code: Label
var _detail: Label


func _ready() -> void:
	set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	# The shared theme styles the two buttons so the error screen reads as one product with the menu.
	theme = UiTheme.make()

	# An opaque cover, so the broken match behind it neither shows through nor takes a click — STOP
	# (not IGNORE, the way the death dim passes clicks) so the dead world below never catches one.
	var backdrop := ColorRect.new()
	backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	backdrop.color = UiTheme.BG
	add_child(backdrop)

	var center := CenterContainer.new()
	center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
	add_child(center)

	var box := VBoxContainer.new()
	box.alignment = BoxContainer.ALIGNMENT_CENTER
	box.add_theme_constant_override("separation", 22)
	center.add_child(box)

	_title = _line(TITLE_FONT_SIZE, TITLE_COLOR)
	box.add_child(_title)

	_code = _line(CODE_FONT_SIZE, UiTheme.ACCENT)
	box.add_child(_code)

	_detail = _line(DETAIL_FONT_SIZE, UiTheme.TEXT_MUTED)
	_detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	_detail.custom_minimum_size = Vector2(DETAIL_MAX_WIDTH, 0.0)
	box.add_child(_detail)

	var row := HBoxContainer.new()
	row.alignment = BoxContainer.ALIGNMENT_CENTER
	row.add_theme_constant_override("separation", 18)
	box.add_child(row)

	var menu_button := Button.new()
	menu_button.text = "Back to Menu"
	menu_button.pressed.connect(func() -> void: menu_requested.emit())
	row.add_child(menu_button)

	var quit_button := Button.new()
	quit_button.text = "Quit"
	quit_button.pressed.connect(func() -> void: quit_requested.emit())
	row.add_child(quit_button)

	hide()


## A centred label at `size` in `color` — the one label shape the screen reuses for its three lines.
func _line(size: int, color: Color) -> Label:
	var label := Label.new()
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	label.add_theme_font_size_override("font_size", size)
	label.add_theme_color_override("font_color", color)
	return label


## Fills the screen for `code` — its headline and badge — and `detail` (the specific what/where),
## then raises it. The driver halts the failed match before calling this, so the screen sits still.
func show_error(code: int, detail: String) -> void:
	_title.text = ErrorCode.title(code)
	_code.text = "Error %s" % ErrorCode.label(code)
	_detail.text = detail
	show()