class_name BuildDrawer extends CanvasLayer ## Phase 17 — Build drawer bottom-sheet. ## ## A mobile-first panel for issuing Designation orders and queuing build jobs. ## Closed state: a 40×40 "+" button at bottom-right (always visible). ## Open state: a full-width panel ~600 px tall with four tabs: ## Designate | Build | Stockpile | Cancel ## ## Tapping any tool button calls Designation.set_active_tool(), emits ## EventBus.alert_added(&"info", …) and auto-closes the drawer so the player ## can begin painting tiles immediately. ## ## Layer 16 — above storyteller banner (15), below modal (20). # ── layout constants ───────────────────────────────────────────────────────── const LAYER_ORDER: int = 16 const PANEL_HEIGHT: int = 600 const BTN_SIZE: int = 80 # preferred hit area for build buttons const FAB_SIZE: int = 48 # floating action button (open trigger) const TAB_HEIGHT: int = 48 const LABEL_HEIGHT: int = 20 const FLOW_COLS: int = 4 # buttons per row in the flow grid # ── tab indices ────────────────────────────────────────────────────────────── const TAB_DESIGNATE: int = 0 const TAB_BUILD: int = 1 const TAB_STOCKPILE: int = 2 const TAB_CANCEL: int = 3 # ── state ──────────────────────────────────────────────────────────────────── var _open: bool = false var _active_tab: int = TAB_DESIGNATE # ── node refs (built at runtime) ───────────────────────────────────────────── var _fab: Button = null # floating ⊕ button (always visible) var _panel: PanelContainer = null var _close_btn: Button = null var _tab_btns: Array[Button] = [] var _tab_containers: Array[Control] = [] # ── build-wall material chooser state ──────────────────────────────────────── # When the player first taps the Stone/Wood Wall button we show an inline # material row; a second tap on the same button commits the choice. var _wall_pending_mat: StringName = &"" var _floor_pending_mat: StringName = &"" var _wall_mat_row: HBoxContainer = null var _floor_mat_row: HBoxContainer = null ## Injected by main.gd; the shared Designation controller on the World node. var designation: Designation = null func _ready() -> void: layer = LAYER_ORDER _build_ui() _set_panel_visible(false) Audit.log("build_drawer", "BuildDrawer ready (layer %d)" % layer) # ── public API ─────────────────────────────────────────────────────────────── func open() -> void: _set_panel_visible(true) Audit.log("build_drawer", "opened (tab=%d)" % _active_tab) func close() -> void: _set_panel_visible(false) Audit.log("build_drawer", "closed") func toggle() -> void: if _open: close() else: open() # ── UI construction ────────────────────────────────────────────────────────── func _build_ui() -> void: # Root control — full viewport anchor so anchors on children work. var root := Control.new() root.name = "Root" root.set_anchors_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(root) # Floating action button — bottom-right, always visible. _fab = Button.new() _fab.name = "FAB" _fab.text = "+" _fab.custom_minimum_size = Vector2(FAB_SIZE, FAB_SIZE) _fab.focus_mode = Control.FOCUS_NONE _fab.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) _fab.offset_left = -FAB_SIZE - 8 _fab.offset_right = -8 _fab.offset_top = -FAB_SIZE - 8 _fab.offset_bottom = -8 _fab.pressed.connect(toggle) root.add_child(_fab) # Panel — full-width, anchored to the bottom of the screen. _panel = PanelContainer.new() _panel.name = "BuildPanel" _panel.set_anchors_preset(Control.PRESET_BOTTOM_WIDE) _panel.offset_top = -PANEL_HEIGHT _panel.offset_bottom = 0 root.add_child(_panel) var vbox := VBoxContainer.new() vbox.add_theme_constant_override("separation", 0) _panel.add_child(vbox) # ── header row (tabs + close button) ──────────────────────────────────── var header := HBoxContainer.new() header.name = "Header" header.custom_minimum_size = Vector2(0, TAB_HEIGHT) header.add_theme_constant_override("separation", 2) vbox.add_child(header) var tab_names: Array[StringName] = [ &"ui.build_drawer.designate", &"ui.build_drawer.build", &"ui.build_drawer.stockpile", &"ui.build_drawer.cancel", ] _tab_btns.clear() for i in tab_names.size(): var tb := Button.new() tb.text = Strings.t(tab_names[i]) tb.custom_minimum_size = Vector2(0, TAB_HEIGHT) tb.focus_mode = Control.FOCUS_NONE tb.size_flags_horizontal = Control.SIZE_EXPAND_FILL var idx := i # capture for closure tb.pressed.connect(func() -> void: _select_tab(idx)) header.add_child(tb) _tab_btns.append(tb) _close_btn = Button.new() _close_btn.name = "CloseBtn" _close_btn.text = "X" _close_btn.custom_minimum_size = Vector2(TAB_HEIGHT, TAB_HEIGHT) _close_btn.focus_mode = Control.FOCUS_NONE _close_btn.pressed.connect(close) header.add_child(_close_btn) # ── scroll area for tab content ────────────────────────────────────────── var scroll := ScrollContainer.new() scroll.name = "Scroll" scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED vbox.add_child(scroll) var content_stack := HBoxContainer.new() content_stack.name = "ContentStack" content_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL # We only show one tab at a time by hiding the others. scroll.add_child(content_stack) # Build each tab panel. _tab_containers.clear() _tab_containers.append(_build_designate_tab()) _tab_containers.append(_build_build_tab()) _tab_containers.append(_build_stockpile_tab()) _tab_containers.append(_build_cancel_tab()) for tc in _tab_containers: tc.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_stack.add_child(tc) _select_tab(TAB_DESIGNATE) func _build_designate_tab() -> Control: var box := VBoxContainer.new() box.name = "DesignateTab" box.add_theme_constant_override("separation", 8) var flow := _make_flow_grid() box.add_child(flow) _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),&"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) return box func _build_build_tab() -> Control: var box := VBoxContainer.new() box.name = "BuildTab" box.add_theme_constant_override("separation", 8) var flow := _make_flow_grid() box.add_child(flow) # Wall — show material chooser on first tap. _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), &"build_wall_stone", func() -> void: _activate_wall(&"stone")) _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), &"build_wall_wood", func() -> void: _activate_wall(&"wood")) # Floor. _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), &"build_floor_wood", func() -> void: _activate_floor(&"wood")) _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), &"build_floor_stone", func() -> void: _activate_floor(&"stone")) # Door + Crate. _add_tool_btn(flow, Strings.t(&"tool.build_door"), &"build_door", func() -> void: _activate(&"build_door", &"", Strings.t(&"tool.build_door"))) _add_tool_btn(flow, Strings.t(&"tool.build_crate"), &"build_crate", func() -> void: _activate(&"build_crate", &"", Strings.t(&"tool.build_crate"))) # Bed + Torch. _add_tool_btn(flow, Strings.t(&"tool.build_bed"), &"build_bed", func() -> void: _activate(&"build_bed", &"", Strings.t(&"tool.build_bed"))) _add_tool_btn(flow, Strings.t(&"tool.build_torch"), &"build_torch", func() -> void: _activate(&"build_torch", &"", Strings.t(&"tool.build_torch"))) # Workbenches. _add_tool_btn(flow, Strings.t(&"tool.workbench_carpenter"), &"build_workbench_carpenter", func() -> void: _activate(&"build_workbench_carpenter", &"", Strings.t(&"tool.workbench_carpenter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_smelter"), &"build_workbench_smelter", func() -> void: _activate(&"build_workbench_smelter", &"", Strings.t(&"tool.workbench_smelter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_millstone"), &"build_workbench_millstone", func() -> void: _activate(&"build_workbench_millstone", &"", Strings.t(&"tool.workbench_millstone"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_hearth"), &"build_workbench_hearth", func() -> void: _activate(&"build_workbench_hearth", &"", Strings.t(&"tool.workbench_hearth"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_cremation_pyre"), &"build_workbench_cremation_pyre", func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre"))) return box func _build_stockpile_tab() -> Control: var box := VBoxContainer.new() box.name = "StockpileTab" box.add_theme_constant_override("separation", 8) var flow := _make_flow_grid() box.add_child(flow) _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), &"paint_stockpile", func() -> void: _activate(&"paint_stockpile", &"", Strings.t(&"tool.stockpile_general"))) _add_tool_btn(flow, Strings.t(&"tool.graveyard"), &"graveyard", func() -> void: _activate(&"graveyard", &"", Strings.t(&"tool.graveyard"))) return box func _build_cancel_tab() -> Control: var box := VBoxContainer.new() box.name = "CancelTab" box.add_theme_constant_override("separation", 8) var lbl := Label.new() lbl.text = Strings.t(&"ui.build_drawer.cancel") lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.add_theme_constant_override("margin_top", 16) box.add_child(lbl) var btn := Button.new() btn.text = Strings.t(&"ui.cancel") btn.custom_minimum_size = Vector2(200, BTN_SIZE) btn.focus_mode = Control.FOCUS_NONE var cancel_hbox := HBoxContainer.new() cancel_hbox.alignment = BoxContainer.ALIGNMENT_CENTER cancel_hbox.add_child(btn) box.add_child(cancel_hbox) btn.pressed.connect(_on_cancel_pressed) return box # ── helpers — UI factories ─────────────────────────────────────────────────── func _make_flow_grid() -> GridContainer: var g := GridContainer.new() g.columns = FLOW_COLS g.add_theme_constant_override("h_separation", 8) g.add_theme_constant_override("v_separation", 8) return g const _THUMB_SCRIPT: Script = preload("res://scenes/ui/build_drawer_thumb.gd") ## Add a single tool button to `container`. The button is a VBoxContainer of ## [thumb preview + Label] wrapped in a Button so the whole cell is one touch ## target. `tool_id` drives the procedural preview shape (BuildDrawerThumb). func _add_tool_btn(container: Control, label_text: String, tool_id: StringName, callback: Callable) -> void: var btn := Button.new() btn.custom_minimum_size = Vector2(BTN_SIZE, BTN_SIZE + LABEL_HEIGHT) btn.focus_mode = Control.FOCUS_NONE var vb := VBoxContainer.new() vb.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_theme_constant_override("separation", 2) # Procedural preview of the entity this tool builds. var thumb := Control.new() thumb.set_script(_THUMB_SCRIPT) # Use .set() — the static type is Control (set_script doesn't refine it), # but the runtime instance has the tool_id property from the script. thumb.set("tool_id", tool_id) thumb.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) thumb.size_flags_horizontal = Control.SIZE_SHRINK_CENTER thumb.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_child(thumb) # Label. var lbl := Label.new() lbl.text = label_text lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lbl.custom_minimum_size = Vector2(BTN_SIZE, LABEL_HEIGHT) lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_child(lbl) btn.add_child(vb) btn.pressed.connect(callback) container.add_child(btn) # ── helpers — tab switching ────────────────────────────────────────────────── func _select_tab(idx: int) -> void: _active_tab = idx for i in _tab_containers.size(): _tab_containers[i].visible = (i == idx) for i in _tab_btns.size(): _tab_btns[i].modulate = Color(1.2, 1.2, 0.8) if i == idx else Color.WHITE # ── helpers — tool activation ──────────────────────────────────────────────── ## Activate `tool_id`, optionally set `mat` on the Designation controller, emit ## the alert feedback, and auto-close the drawer. func _activate(tool_id: StringName, mat: StringName, display_name: String) -> void: if designation == null: Audit.log("build_drawer", "no Designation ref — cannot activate tool '%s'" % tool_id) return designation.tool_material = mat designation.set_active_tool(tool_id) EventBus.alert_added.emit(&"info", "Tool: %s" % display_name, Vector2i(-1, -1)) close() Audit.log("build_drawer", "activated tool '%s' (mat='%s')" % [tool_id, mat]) func _activate_wall(mat: StringName) -> void: var display: String = Strings.t(&"tool.build_wall_stone") if mat == &"stone" \ else Strings.t(&"tool.build_wall_wood") _activate(&"build_wall", mat, display) func _activate_floor(mat: StringName) -> void: var display: String = Strings.t(&"tool.build_floor_wood") if mat == &"wood" \ else Strings.t(&"tool.build_floor_stone") _activate(&"build_floor", mat, display) func _on_cancel_pressed() -> void: if designation != null: designation.tool_material = &"" designation.set_active_tool(Designation.TOOL_NONE) EventBus.alert_added.emit(&"info", "Tool: None", Vector2i(-1, -1)) close() Audit.log("build_drawer", "tool cancelled") # ── keyboard input ─────────────────────────────────────────────────────────── func _unhandled_input(event: InputEvent) -> void: # B — toggle the build drawer. if event.is_action_pressed("open_build"): toggle() get_viewport().set_input_as_handled() return # Escape — close if open (panel Escape runs before Selection deselect). if event.is_action_pressed("cancel") and _open: close() get_viewport().set_input_as_handled() return # ── helpers — visibility ───────────────────────────────────────────────────── func _set_panel_visible(v: bool) -> void: _open = v if _panel != null: _panel.visible = v # Keep the FAB visible at all times.