extends CanvasLayer ## In-game HUD: crosshair, interaction prompt, hotbar, toasts, and the ## inventory screen. Rendering only: it reads player state and forwards ## clicks to Actions. const UITheme = preload("res://scripts/ui/theme.gd") const SlotScript = preload("res://scripts/ui/slot.gd") const InventoryScreen = preload("res://scripts/ui/inventory_screen.gd") const PauseMenu = preload("res://scripts/ui/pause_menu.gd") var player: CharacterBody3D var _prompt: Label var _held_name: Label var _hotbar: Array = [] var _toasts: VBoxContainer var _screen: Control var _menu: Control func bind(p: CharacterBody3D) -> void: player = p layer = 5 var root := Control.new() root.set_anchors_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(root) # Crosshair. var dot := ColorRect.new() dot.color = Color(1, 1, 1, 0.8) dot.size = Vector2(4, 4) dot.set_anchors_preset(Control.PRESET_CENTER) dot.position -= Vector2(2, 2) dot.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(dot) _prompt = UITheme.label("", 18) _prompt.set_anchors_and_offsets_preset(Control.PRESET_CENTER) _prompt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _prompt.grow_horizontal = Control.GROW_DIRECTION_BOTH _prompt.offset_top = 28 _prompt.offset_bottom = 56 root.add_child(_prompt) # Hotbar. var bar := HBoxContainer.new() bar.add_theme_constant_override("separation", 6) bar.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM) bar.grow_horizontal = Control.GROW_DIRECTION_BOTH bar.grow_vertical = Control.GROW_DIRECTION_BEGIN bar.offset_bottom = -18 bar.mouse_filter = Control.MOUSE_FILTER_IGNORE for i in range(8): var s: Panel = SlotScript.new(i, 64, str(i + 1)) s.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(s) _hotbar.append(s) root.add_child(bar) _held_name = UITheme.label("", 15, UITheme.TEXT_DIM) _held_name.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM) _held_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _held_name.grow_horizontal = Control.GROW_DIRECTION_BOTH _held_name.offset_top = -112 _held_name.offset_bottom = -90 root.add_child(_held_name) _toasts = VBoxContainer.new() _toasts.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT) _toasts.grow_horizontal = Control.GROW_DIRECTION_BEGIN _toasts.grow_vertical = Control.GROW_DIRECTION_BEGIN _toasts.offset_right = -24 _toasts.offset_bottom = -110 _toasts.alignment = BoxContainer.ALIGNMENT_END _toasts.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(_toasts) _screen = InventoryScreen.new() root.add_child(_screen) _screen.bind(player) _menu = PauseMenu.new() root.add_child(_menu) _menu.bind(player) process_mode = Node.PROCESS_MODE_ALWAYS # so ESC works while paused _screen.process_mode = Node.PROCESS_MODE_PAUSABLE player.inventory.changed.connect(_refresh) player.selected_changed.connect(_refresh) player.inventory_requested.connect(_on_inventory_requested) player.hands.prompt_changed.connect(func(t): _prompt.text = t) Actions.notify.connect(toast) ItemDB.icons_ready.connect(_refresh) _refresh() func _on_inventory_requested(container: Node) -> void: if not _screen.is_open(): _screen.open(container) ## ESC: close the inventory if it's open, otherwise toggle the pause menu. func _input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE: if _screen.is_open(): _screen.close() elif _menu.is_open(): _menu.back() else: _menu.open() get_viewport().set_input_as_handled() func _unhandled_input(event: InputEvent) -> void: if _menu.is_open(): return if event.is_action_pressed("inventory") and not _screen.is_open(): _screen.open(null) get_viewport().set_input_as_handled() func _refresh() -> void: for i in range(_hotbar.size()): _hotbar[i].set_stack(player.inventory.get_slot(i)) _hotbar[i].set_selected(i == player.selected) var id: String = player.held_item_id() _held_name.text = ItemDB.item_name(id) if not id.is_empty() else "" func toast(text: String) -> void: # Merge repeated "+N Item" toasts so chopping doesn't spam the corner. for c in _toasts.get_children(): if c.get_meta("base", "") == text: c.modulate.a = 1.0 return var l := UITheme.label(text, 17) l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT l.set_meta("base", text) _toasts.add_child(l) while _toasts.get_child_count() > 6: _toasts.get_child(0).free() var tw := l.create_tween() tw.tween_interval(2.2) tw.tween_property(l, "modulate:a", 0.0, 0.8) tw.tween_callback(l.queue_free)