class_name GameUI extends CanvasLayer ## GameUI - Main UI overlay for game information and controls signal end_phase_pressed signal pass_priority_pressed signal field_card_action_requested(card: CardInstance, zone_type: Enums.ZoneType, player_index: int, action: String) # UI Components var turn_panel: PanelContainer var phase_panel: PanelContainer var cp_panel: PanelContainer var message_panel: PanelContainer var card_detail_panel: PanelContainer var action_buttons: HBoxContainer # Labels var turn_label: Label var phase_label: Label var cp_label: Label var message_label: Label # Card detail labels var detail_name_label: Label var detail_type_label: Label var detail_cost_label: Label var detail_power_label: Label var detail_element_label: Label var detail_ability_label: Label # Field card selection panel var field_card_panel: Panel var field_card_image_container: Control var field_card_action_menu: VBoxContainer var selected_field_card: CardInstance = null var selected_field_zone: Enums.ZoneType = Enums.ZoneType.HAND var selected_field_player: int = -1 # Field card panel sizes (match hand selection panel) const FIELD_CARD_WIDTH: float = 405.0 const FIELD_CARD_HEIGHT: float = 567.0 const FIELD_MENU_WIDTH: float = 180.0 # Buttons var end_phase_button: Button var pass_button: Button # Message queue var message_queue: Array[String] = [] var message_timer: Timer const MESSAGE_DISPLAY_TIME: float = 3.0 func _ready() -> void: _create_ui() _connect_signals() func _create_ui() -> void: # Root control that fills the screen var root = Control.new() add_child(root) root.set_anchors_preset(Control.PRESET_FULL_RECT) root.mouse_filter = Control.MOUSE_FILTER_IGNORE # === TOP BAR === var top_bar = HBoxContainer.new() root.add_child(top_bar) top_bar.set_anchors_preset(Control.PRESET_TOP_WIDE) top_bar.offset_left = 10 top_bar.offset_right = -10 top_bar.offset_top = 10 top_bar.offset_bottom = 70 top_bar.add_theme_constant_override("separation", 20) # Turn panel (top left) turn_panel = _create_panel() top_bar.add_child(turn_panel) var turn_vbox = VBoxContainer.new() turn_panel.add_child(turn_vbox) var turn_header = Label.new() turn_header.text = "Turn" turn_header.add_theme_font_size_override("font_size", 12) turn_vbox.add_child(turn_header) turn_label = Label.new() turn_label.text = "Player 1 - Turn 1" turn_label.add_theme_font_size_override("font_size", 16) turn_vbox.add_child(turn_label) # Phase panel (top center-left) phase_panel = _create_panel() top_bar.add_child(phase_panel) var phase_vbox = VBoxContainer.new() phase_panel.add_child(phase_vbox) var phase_header = Label.new() phase_header.text = "Phase" phase_header.add_theme_font_size_override("font_size", 12) phase_vbox.add_child(phase_header) phase_label = Label.new() phase_label.text = "Active Phase" phase_label.add_theme_font_size_override("font_size", 16) phase_vbox.add_child(phase_label) # CP panel cp_panel = _create_panel() top_bar.add_child(cp_panel) var cp_vbox = VBoxContainer.new() cp_panel.add_child(cp_vbox) var cp_header = Label.new() cp_header.text = "CP Pool" cp_header.add_theme_font_size_override("font_size", 12) cp_vbox.add_child(cp_header) cp_label = Label.new() cp_label.text = "0 CP" cp_label.add_theme_font_size_override("font_size", 16) cp_vbox.add_child(cp_label) # Spacer to push buttons to the right var spacer = Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL top_bar.add_child(spacer) # Action buttons in top bar (right side) action_buttons = HBoxContainer.new() top_bar.add_child(action_buttons) action_buttons.add_theme_constant_override("separation", 10) pass_button = Button.new() pass_button.text = "Pass" pass_button.custom_minimum_size = Vector2(80, 40) action_buttons.add_child(pass_button) end_phase_button = Button.new() end_phase_button.text = "End Phase" end_phase_button.custom_minimum_size = Vector2(100, 40) action_buttons.add_child(end_phase_button) # === MESSAGE PANEL (upper center of screen, below top bar) === # Use a container to properly center the message var message_container = Control.new() root.add_child(message_container) message_container.set_anchors_preset(Control.PRESET_TOP_WIDE) message_container.offset_top = 90 message_container.offset_bottom = 160 message_container.mouse_filter = Control.MOUSE_FILTER_IGNORE var message_center = CenterContainer.new() message_container.add_child(message_center) message_center.set_anchors_preset(Control.PRESET_FULL_RECT) message_center.mouse_filter = Control.MOUSE_FILTER_IGNORE message_panel = _create_panel() message_center.add_child(message_panel) message_panel.custom_minimum_size = Vector2(350, 50) message_label = Label.new() message_label.text = "" message_label.add_theme_font_size_override("font_size", 20) message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER message_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER message_panel.add_child(message_label) message_panel.visible = false # === CARD DETAIL PANEL (right side) === card_detail_panel = _create_panel() root.add_child(card_detail_panel) card_detail_panel.set_anchors_preset(Control.PRESET_CENTER_RIGHT) card_detail_panel.offset_left = -220 card_detail_panel.offset_right = -10 card_detail_panel.offset_top = -150 card_detail_panel.offset_bottom = 150 card_detail_panel.visible = false var detail_vbox = VBoxContainer.new() card_detail_panel.add_child(detail_vbox) detail_vbox.add_theme_constant_override("separation", 5) detail_name_label = Label.new() detail_name_label.text = "Card Name" detail_name_label.add_theme_font_size_override("font_size", 18) detail_vbox.add_child(detail_name_label) var separator = HSeparator.new() detail_vbox.add_child(separator) detail_type_label = Label.new() detail_type_label.text = "Type: Forward" detail_vbox.add_child(detail_type_label) detail_element_label = Label.new() detail_element_label.text = "Element: Fire" detail_vbox.add_child(detail_element_label) detail_cost_label = Label.new() detail_cost_label.text = "Cost: 3" detail_vbox.add_child(detail_cost_label) detail_power_label = Label.new() detail_power_label.text = "Power: 7000" detail_vbox.add_child(detail_power_label) var separator2 = HSeparator.new() detail_vbox.add_child(separator2) var ability_header = Label.new() ability_header.text = "Abilities:" ability_header.add_theme_font_size_override("font_size", 12) detail_vbox.add_child(ability_header) detail_ability_label = Label.new() detail_ability_label.text = "" detail_ability_label.autowrap_mode = TextServer.AUTOWRAP_WORD detail_ability_label.custom_minimum_size.x = 180 detail_vbox.add_child(detail_ability_label) # === FIELD CARD SELECTION PANEL (centered on screen) === _create_field_card_panel(root) # Message timer message_timer = Timer.new() add_child(message_timer) message_timer.one_shot = true message_timer.timeout.connect(_on_message_timer_timeout) func _create_panel() -> PanelContainer: var panel = PanelContainer.new() # Create stylebox var style = StyleBoxFlat.new() style.bg_color = Color(0.1, 0.1, 0.15, 0.9) style.border_color = Color(0.3, 0.3, 0.4) style.set_border_width_all(2) style.set_corner_radius_all(5) style.set_content_margin_all(8) panel.add_theme_stylebox_override("panel", style) return panel func _connect_signals() -> void: end_phase_button.pressed.connect(_on_end_phase_pressed) pass_button.pressed.connect(_on_pass_priority_pressed) # Connect to GameManager signals if GameManager: GameManager.turn_changed.connect(_on_turn_changed) GameManager.phase_changed.connect(_on_phase_changed) GameManager.message.connect(show_message) func _on_end_phase_pressed() -> void: end_phase_pressed.emit() if GameManager: GameManager.pass_priority() func _on_pass_priority_pressed() -> void: pass_priority_pressed.emit() if GameManager: GameManager.pass_priority() func _on_turn_changed(player_name: String, turn_number: int) -> void: turn_label.text = player_name + " - Turn " + str(turn_number) func _on_phase_changed(phase_name: String) -> void: phase_label.text = phase_name ## Update CP display func update_cp_display(cp_pool: CPPool) -> void: if not cp_pool: cp_label.text = "0 CP" return var total = cp_pool.get_total_cp() var text = str(total) + " CP" # Show element breakdown if any specific element CP var elements_text = [] for element in Enums.Element.values(): var amount = cp_pool.get_cp(element) if amount > 0: var elem_name = Enums.element_to_string(element) elements_text.append(elem_name + ":" + str(amount)) if elements_text.size() > 0: text += "\n" + ", ".join(elements_text) cp_label.text = text ## Show a message func show_message(text: String) -> void: message_queue.append(text) if not message_timer.is_stopped(): return _show_next_message() func _show_next_message() -> void: if message_queue.is_empty(): message_panel.visible = false return var text = message_queue.pop_front() message_label.text = text message_panel.visible = true message_timer.start(MESSAGE_DISPLAY_TIME) func _on_message_timer_timeout() -> void: _show_next_message() ## Show card detail panel func show_card_detail(card: CardInstance) -> void: if not card or not card.card_data: card_detail_panel.visible = false return var data = card.card_data detail_name_label.text = data.name detail_type_label.text = "Type: " + Enums.card_type_to_string(data.type) detail_cost_label.text = "Cost: " + str(data.cost) # Element var element_strs = [] for elem in data.elements: element_strs.append(Enums.element_to_string(elem)) detail_element_label.text = "Element: " + "/".join(element_strs) # Power if data.type == Enums.CardType.FORWARD or data.power > 0: detail_power_label.text = "Power: " + str(card.get_power()) detail_power_label.visible = true else: detail_power_label.visible = false # Abilities var ability_text = "" for ability in data.abilities: if ability_text != "": ability_text += "\n\n" var type_str = "" match ability.type: Enums.AbilityType.FIELD: type_str = "[Field]" Enums.AbilityType.AUTO: type_str = "[Auto]" Enums.AbilityType.ACTION: type_str = "[Action]" Enums.AbilityType.SPECIAL: type_str = "[Special]" ability_text += type_str if ability.name != "": ability_text += " " + ability.name ability_text += "\n" + ability.effect detail_ability_label.text = ability_text if ability_text != "" else "No abilities" card_detail_panel.visible = true ## Hide card detail panel func hide_card_detail() -> void: print("DEBUG: GameUI.hide_card_detail() called") card_detail_panel.visible = false ## Update button states based on game phase func update_button_states(can_end_phase: bool, can_pass: bool) -> void: end_phase_button.disabled = not can_end_phase pass_button.disabled = not can_pass ## Create field card selection panel func _create_field_card_panel(root: Control) -> void: field_card_panel = Panel.new() field_card_panel.visible = false field_card_panel.mouse_filter = Control.MOUSE_FILTER_STOP field_card_panel.z_index = 200 var style = StyleBoxFlat.new() style.bg_color = Color(0.08, 0.08, 0.12, 0.95) style.border_color = Color(0.5, 0.4, 0.2) style.set_border_width_all(2) style.set_corner_radius_all(6) style.content_margin_left = 15 style.content_margin_right = 15 style.content_margin_top = 15 style.content_margin_bottom = 15 field_card_panel.add_theme_stylebox_override("panel", style) root.add_child(field_card_panel) var hbox = HBoxContainer.new() hbox.set_anchors_preset(Control.PRESET_FULL_RECT) hbox.add_theme_constant_override("separation", 20) field_card_panel.add_child(hbox) # Card image field_card_image_container = Control.new() field_card_image_container.custom_minimum_size = Vector2(FIELD_CARD_WIDTH, FIELD_CARD_HEIGHT) field_card_image_container.clip_contents = false hbox.add_child(field_card_image_container) # Action menu field_card_action_menu = VBoxContainer.new() field_card_action_menu.custom_minimum_size = Vector2(FIELD_MENU_WIDTH, FIELD_CARD_HEIGHT) field_card_action_menu.add_theme_constant_override("separation", 10) hbox.add_child(field_card_action_menu) var panel_width = 15 + FIELD_CARD_WIDTH + 20 + FIELD_MENU_WIDTH + 15 var panel_height = 15 + FIELD_CARD_HEIGHT + 15 field_card_panel.custom_minimum_size = Vector2(panel_width, panel_height) field_card_panel.size = Vector2(panel_width, panel_height) ## Show field card selection panel with context-sensitive actions func show_field_card_selection(card: CardInstance, zone_type: Enums.ZoneType, player_index: int) -> void: if not card or not card.card_data: return selected_field_card = card selected_field_zone = zone_type selected_field_player = player_index # Clear previous content for child in field_card_image_container.get_children(): child.queue_free() for child in field_card_action_menu.get_children(): child.queue_free() # Card image var texture = CardDatabase.get_card_texture(card.card_data) if texture: var tex_rect = TextureRect.new() tex_rect.texture = texture tex_rect.set_anchors_preset(Control.PRESET_FULL_RECT) tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE tex_rect.stretch_mode = TextureRect.STRETCH_SCALE tex_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE field_card_image_container.add_child(tex_rect) else: var color_rect = ColorRect.new() color_rect.set_anchors_preset(Control.PRESET_FULL_RECT) color_rect.color = Enums.get_element_color(card.card_data.get_primary_element()).lightened(0.4) field_card_image_container.add_child(color_rect) # Context-sensitive action buttons var is_current_player = GameManager.game_state and player_index == GameManager.game_state.turn_manager.current_player_index var phase = GameManager.get_current_phase() if GameManager.game_state else Enums.TurnPhase.ACTIVE if is_current_player: match zone_type: Enums.ZoneType.FIELD_BACKUPS: if not card.is_dull() and (phase == Enums.TurnPhase.MAIN_1 or phase == Enums.TurnPhase.MAIN_2): _add_field_action_button("Dull for CP", "dull_cp") Enums.ZoneType.FIELD_FORWARDS: if card.can_attack() and phase == Enums.TurnPhase.ATTACK: _add_field_action_button("Attack", "attack") # Always show card info _add_field_card_info(card) _add_field_action_button("Close", "cancel") # Position same as hand selection panel: centered horizontally, above hand area var viewport = get_viewport() if viewport: var vp_size = viewport.get_visible_rect().size var panel_w = field_card_panel.size.x var panel_h = field_card_panel.size.y # Center horizontally on screen var panel_x = (vp_size.x - panel_w) / 2.0 # Position above the hand area (hand cards are ~373px from bottom) # Match hand panel: bottom of panel sits ~20px above hand cards var hand_top_y = vp_size.y - 100.0 - 273.0 # Same calc as Main._position_hand_display var panel_y = hand_top_y - panel_h - 20.0 # Clamp to stay on screen (don't go above top bar) if panel_y < 90.0: panel_y = 90.0 field_card_panel.position = Vector2(panel_x, panel_y) # Hide the old detail panel card_detail_panel.visible = false field_card_panel.visible = true ## Add card info labels to the action menu func _add_field_card_info(card: CardInstance) -> void: var data = card.card_data # Card name header var name_label = Label.new() name_label.text = data.name name_label.add_theme_font_size_override("font_size", 16) name_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7)) field_card_action_menu.add_child(name_label) # Type / Element / Cost var info_label = Label.new() var type_str = Enums.card_type_to_string(data.type) var elem_strs = [] for elem in data.elements: elem_strs.append(Enums.element_to_string(elem)) info_label.text = type_str + " | " + "/".join(elem_strs) + " | Cost: " + str(data.cost) info_label.add_theme_font_size_override("font_size", 12) info_label.autowrap_mode = TextServer.AUTOWRAP_WORD field_card_action_menu.add_child(info_label) # Power (if applicable) if data.type == Enums.CardType.FORWARD or data.power > 0: var power_label = Label.new() power_label.text = "Power: " + str(card.get_power()) power_label.add_theme_font_size_override("font_size", 12) field_card_action_menu.add_child(power_label) # State var state_label = Label.new() state_label.text = "Status: " + ("Dull" if card.is_dull() else "Active") state_label.add_theme_font_size_override("font_size", 12) field_card_action_menu.add_child(state_label) # Separator before abilities var sep = HSeparator.new() field_card_action_menu.add_child(sep) # Abilities if data.abilities.size() > 0: for ability in data.abilities: var ability_label = Label.new() var type_prefix = "" match ability.type: Enums.AbilityType.FIELD: type_prefix = "[Field] " Enums.AbilityType.AUTO: type_prefix = "[Auto] " Enums.AbilityType.ACTION: type_prefix = "[Action] " Enums.AbilityType.SPECIAL: type_prefix = "[Special] " ability_label.text = type_prefix + ability.effect ability_label.add_theme_font_size_override("font_size", 11) ability_label.autowrap_mode = TextServer.AUTOWRAP_WORD ability_label.custom_minimum_size.x = FIELD_MENU_WIDTH - 10 field_card_action_menu.add_child(ability_label) else: var no_ability = Label.new() no_ability.text = "No abilities" no_ability.add_theme_font_size_override("font_size", 11) field_card_action_menu.add_child(no_ability) var sep2 = HSeparator.new() field_card_action_menu.add_child(sep2) ## Add a styled action button to the field card panel func _add_field_action_button(text: String, action: String) -> void: var button = Button.new() button.text = text button.custom_minimum_size = Vector2(FIELD_MENU_WIDTH - 10, 40) var style_normal = StyleBoxFlat.new() style_normal.bg_color = Color(0.25, 0.25, 0.3) style_normal.border_color = Color(0.5, 0.5, 0.6) style_normal.set_border_width_all(1) style_normal.set_corner_radius_all(5) style_normal.content_margin_top = 8 style_normal.content_margin_bottom = 8 button.add_theme_stylebox_override("normal", style_normal) var style_hover = StyleBoxFlat.new() style_hover.bg_color = Color(0.35, 0.35, 0.45) style_hover.border_color = Color(0.7, 0.6, 0.3) style_hover.set_border_width_all(2) style_hover.set_corner_radius_all(5) style_hover.content_margin_top = 8 style_hover.content_margin_bottom = 8 button.add_theme_stylebox_override("hover", style_hover) var style_pressed = StyleBoxFlat.new() style_pressed.bg_color = Color(0.2, 0.2, 0.25) style_pressed.border_color = Color(0.7, 0.6, 0.3) style_pressed.set_border_width_all(2) style_pressed.set_corner_radius_all(5) style_pressed.content_margin_top = 8 style_pressed.content_margin_bottom = 8 button.add_theme_stylebox_override("pressed", style_pressed) button.add_theme_font_size_override("font_size", 14) button.pressed.connect(_on_field_action_pressed.bind(action)) field_card_action_menu.add_child(button) func _on_field_action_pressed(action: String) -> void: if action == "cancel": hide_field_card_selection() return if selected_field_card: field_card_action_requested.emit(selected_field_card, selected_field_zone, selected_field_player, action) hide_field_card_selection() ## Hide field card selection panel func hide_field_card_selection() -> void: field_card_panel.visible = false selected_field_card = null