class_name HandDisplay extends Control ## HandDisplay - Shows the current player's hand in a fan layout ## Click a card to select it and show action menu signal card_action_requested(card_instance: CardInstance, action: String) signal card_hovered(card_instance: CardInstance) signal card_unhovered signal card_selected(card_instance: CardInstance) # Emitted when selection panel opens signal hand_minimized_changed(minimized: bool) # Emitted when minimize state changes # Hand card visuals var hand_cards: Array[Control] = [] var card_instances: Array[CardInstance] = [] # Layout settings - FF-TCG card ratio is approximately 63:88 const CARD_WIDTH: float = 195.0 # Hand cards (triple the original 65) const CARD_HEIGHT: float = 273.0 # Matches ~63:88 ratio (triple the original 91) const CARD_OVERLAP: float = 100.0 # Increased overlap for larger cards const FAN_ANGLE: float = 1.0 # Degrees per card from center const HOVER_LIFT: float = 15.0 # Selected card display settings (double the original 180x252) const SELECTED_CARD_WIDTH: float = 405.0 const SELECTED_CARD_HEIGHT: float = 567.0 const MENU_WIDTH: float = 180.0 # Current state var hovered_card_index: int = -1 var selected_card_index: int = -1 var selected_card_panel: Panel = null var card_image_container: Control = null var action_menu: VBoxContainer = null # Minimized hand state var is_minimized: bool = false var minimized_bar: HBoxContainer = null var minimized_items: Array = [] var toggle_button: Button = null const MINIMIZED_BAR_HEIGHT: float = 30.0 const MINIMIZED_ITEM_GAP: float = 4.0 # Debug visualization (set to true to see positioning markers) const DEBUG_MARKERS: bool = false var debug_container_outline: ColorRect = null func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE clip_contents = false # Allow cards to extend beyond container bounds resized.connect(_on_resized) _create_selection_ui() _create_toggle_button() _create_minimized_bar() if DEBUG_MARKERS: _create_debug_markers() func _create_selection_ui() -> void: # Create the selected card display panel using HBoxContainer for reliable layout selected_card_panel = Panel.new() selected_card_panel.visible = false selected_card_panel.mouse_filter = Control.MOUSE_FILTER_STOP selected_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) # Add padding via content margins style.content_margin_left = 15 style.content_margin_right = 15 style.content_margin_top = 15 style.content_margin_bottom = 15 selected_card_panel.add_theme_stylebox_override("panel", style) add_child(selected_card_panel) # Use HBoxContainer inside the panel for horizontal layout var hbox = HBoxContainer.new() hbox.set_anchors_preset(Control.PRESET_FULL_RECT) hbox.add_theme_constant_override("separation", 20) # Gap between card and menu selected_card_panel.add_child(hbox) # Card image container with fixed size card_image_container = Control.new() card_image_container.custom_minimum_size = Vector2(SELECTED_CARD_WIDTH, SELECTED_CARD_HEIGHT) card_image_container.name = "CardContainer" card_image_container.clip_contents = false hbox.add_child(card_image_container) # Action menu - VBoxContainer for vertical button layout action_menu = VBoxContainer.new() action_menu.custom_minimum_size = Vector2(MENU_WIDTH, SELECTED_CARD_HEIGHT) action_menu.add_theme_constant_override("separation", 10) hbox.add_child(action_menu) # Set panel size based on contents (padding + card + gap + menu + padding) var panel_width = 15 + SELECTED_CARD_WIDTH + 20 + MENU_WIDTH + 15 var panel_height = 15 + SELECTED_CARD_HEIGHT + 15 selected_card_panel.custom_minimum_size = Vector2(panel_width, panel_height) selected_card_panel.size = Vector2(panel_width, panel_height) func _create_toggle_button() -> void: toggle_button = Button.new() toggle_button.text = "Hide Hand" toggle_button.custom_minimum_size = Vector2(90, 26) toggle_button.size = Vector2(90, 26) toggle_button.mouse_filter = Control.MOUSE_FILTER_STOP toggle_button.z_index = 150 toggle_button.add_theme_font_size_override("font_size", 11) toggle_button.add_theme_color_override("font_color", Color(0.8, 0.75, 0.65)) toggle_button.add_theme_color_override("font_hover_color", Color(1.0, 0.95, 0.8)) var style_normal = StyleBoxFlat.new() style_normal.bg_color = Color(0.12, 0.12, 0.16, 0.85) style_normal.border_color = Color(0.4, 0.35, 0.25) style_normal.set_border_width_all(1) style_normal.set_corner_radius_all(4) toggle_button.add_theme_stylebox_override("normal", style_normal) var style_hover = StyleBoxFlat.new() style_hover.bg_color = Color(0.18, 0.18, 0.24, 0.9) style_hover.border_color = Color(0.6, 0.5, 0.3) style_hover.set_border_width_all(1) style_hover.set_corner_radius_all(4) toggle_button.add_theme_stylebox_override("hover", style_hover) var style_pressed = StyleBoxFlat.new() style_pressed.bg_color = Color(0.08, 0.08, 0.1, 0.9) style_pressed.border_color = Color(0.6, 0.5, 0.3) style_pressed.set_border_width_all(1) style_pressed.set_corner_radius_all(4) toggle_button.add_theme_stylebox_override("pressed", style_pressed) toggle_button.pressed.connect(_on_toggle_hand) add_child(toggle_button) func _create_minimized_bar() -> void: minimized_bar = HBoxContainer.new() minimized_bar.visible = false minimized_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE minimized_bar.add_theme_constant_override("separation", int(MINIMIZED_ITEM_GAP)) minimized_bar.z_index = 10 add_child(minimized_bar) func _on_toggle_hand() -> void: is_minimized = not is_minimized if is_minimized: toggle_button.text = "Show Hand" _deselect_card() # Cards stay visible — Main.gd will reposition so only tops peek up else: toggle_button.text = "Hide Hand" _layout_cards() _position_toggle_button() hand_minimized_changed.emit(is_minimized) func _update_minimized_bar() -> void: # Clear previous items for child in minimized_bar.get_children(): child.queue_free() minimized_items.clear() for i in range(card_instances.size()): var card = card_instances[i] var item = _create_minimized_item(card, i) minimized_bar.add_child(item) minimized_items.append(item) # Position the bar centered at the bottom call_deferred("_position_minimized_bar") func _create_minimized_item(card: CardInstance, index: int) -> PanelContainer: var item = PanelContainer.new() item.mouse_filter = Control.MOUSE_FILTER_STOP var style = StyleBoxFlat.new() style.bg_color = Color(0.1, 0.1, 0.14, 0.9) style.border_color = Color(0.3, 0.3, 0.35) style.set_border_width_all(1) style.set_corner_radius_all(3) style.content_margin_left = 4 style.content_margin_right = 8 style.content_margin_top = 2 style.content_margin_bottom = 2 item.add_theme_stylebox_override("panel", style) var hbox = HBoxContainer.new() hbox.add_theme_constant_override("separation", 5) hbox.mouse_filter = Control.MOUSE_FILTER_IGNORE item.add_child(hbox) # Cost crystal — element-colored circle with cost number var crystal = Panel.new() crystal.custom_minimum_size = Vector2(22, 22) crystal.mouse_filter = Control.MOUSE_FILTER_IGNORE var crystal_style = StyleBoxFlat.new() var elem_color = Enums.get_element_color(card.card_data.get_primary_element()) crystal_style.bg_color = elem_color crystal_style.set_corner_radius_all(11) crystal_style.set_border_width_all(1) crystal_style.border_color = elem_color.lightened(0.3) crystal.add_theme_stylebox_override("panel", crystal_style) var cost_label = Label.new() cost_label.text = str(card.card_data.cost) cost_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER cost_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER cost_label.set_anchors_preset(Control.PRESET_FULL_RECT) cost_label.add_theme_font_size_override("font_size", 12) cost_label.add_theme_color_override("font_color", Color.WHITE) cost_label.mouse_filter = Control.MOUSE_FILTER_IGNORE crystal.add_child(cost_label) hbox.add_child(crystal) # Card name var name_label = Label.new() name_label.text = card.card_data.name name_label.add_theme_font_size_override("font_size", 12) name_label.add_theme_color_override("font_color", Color(0.85, 0.82, 0.72)) name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE hbox.add_child(name_label) # Click to expand hand and select this card item.gui_input.connect(_on_minimized_item_input.bind(index)) return item func _on_minimized_item_input(event: InputEvent, index: int) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: # Expand hand and select the clicked card is_minimized = false toggle_button.text = "Hide Hand" minimized_bar.visible = false for card_ui in hand_cards: card_ui.visible = true _layout_cards() _position_toggle_button() hand_minimized_changed.emit(false) _select_card(index) func _position_toggle_button() -> void: if not toggle_button: return var display_width = size.x if display_width <= 0: var viewport = get_viewport() if viewport: display_width = viewport.get_visible_rect().size.x - 20 # Position toggle button: right side, above hand area if is_minimized: # Above the minimized bar toggle_button.position = Vector2(display_width - toggle_button.size.x - 5, -toggle_button.size.y - 4) else: # Above the full hand toggle_button.position = Vector2(display_width - toggle_button.size.x - 5, -toggle_button.size.y - 4) func _position_minimized_bar() -> void: if not minimized_bar: return var display_width = size.x if display_width <= 0: var viewport = get_viewport() if viewport: display_width = viewport.get_visible_rect().size.x - 20 # Center the bar horizontally, positioned at top of hand container area var bar_width = minimized_bar.size.x minimized_bar.position = Vector2((display_width - bar_width) / 2.0, 0) func _create_debug_markers() -> void: # Create a visible outline around the hand container bounds # RED = hand container area debug_container_outline = ColorRect.new() debug_container_outline.color = Color(1, 0, 0, 0.3) # Semi-transparent red debug_container_outline.mouse_filter = Control.MOUSE_FILTER_IGNORE debug_container_outline.z_index = -1 # Behind cards add_child(debug_container_outline) # GREEN line = where card bottoms should be (base_y + CARD_HEIGHT = 0 + 91 = 91) var card_bottom_line = ColorRect.new() card_bottom_line.color = Color(0, 1, 0, 0.8) # Bright green card_bottom_line.position = Vector2(0, CARD_HEIGHT) # y = 91 card_bottom_line.size = Vector2(2000, 3) # Thin horizontal line card_bottom_line.mouse_filter = Control.MOUSE_FILTER_IGNORE card_bottom_line.z_index = 300 add_child(card_bottom_line) # BLUE line = where card tops should be (base_y = 0) var card_top_line = ColorRect.new() card_top_line.color = Color(0, 0, 1, 0.8) # Bright blue card_top_line.position = Vector2(0, 0) card_top_line.size = Vector2(2000, 3) card_top_line.mouse_filter = Control.MOUSE_FILTER_IGNORE card_top_line.z_index = 300 add_child(card_top_line) # Print positioning info call_deferred("_print_debug_info") func _print_debug_info() -> void: var viewport = get_viewport() var vp_size = viewport.get_visible_rect().size if viewport else Vector2.ZERO print("=== HAND DISPLAY DEBUG ===") print("Viewport size: ", vp_size) print("HandDisplay global_position: ", global_position) print("HandDisplay size: ", size) print("Card base_y in container: 0") print("Card height: ", CARD_HEIGHT) print("Card global top: ", global_position.y) print("Card global bottom: ", global_position.y + CARD_HEIGHT) print("Distance from viewport bottom: ", vp_size.y - (global_position.y + CARD_HEIGHT)) print("=== END DEBUG ===") func _on_resized() -> void: _layout_cards() if DEBUG_MARKERS and debug_container_outline: # Update debug outline to match container size debug_container_outline.position = Vector2.ZERO debug_container_outline.size = size call_deferred("_print_debug_info") ## Update hand display with cards func update_hand(cards: Array) -> void: # Clear existing for card_ui in hand_cards: card_ui.queue_free() hand_cards.clear() card_instances.clear() # Deselect any selected card _deselect_card() # Store card instances for card in cards: if card is CardInstance: card_instances.append(card) # Create card UI elements _create_card_visuals() # Cards stay visible even when minimized — Main.gd handles positioning # Defer layout to ensure size is set call_deferred("_layout_cards") func _create_card_visuals() -> void: for i in range(card_instances.size()): var card = card_instances[i] var card_ui = _create_card_ui(card, i) add_child(card_ui) hand_cards.append(card_ui) func _create_card_ui(card: CardInstance, index: int) -> Control: var container = Panel.new() container.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT) container.size = Vector2(CARD_WIDTH, CARD_HEIGHT) container.mouse_filter = Control.MOUSE_FILTER_STOP var style = StyleBoxFlat.new() style.bg_color = Color.TRANSPARENT style.border_color = Color(0.2, 0.2, 0.2) style.set_border_width_all(1) style.set_corner_radius_all(4) container.add_theme_stylebox_override("panel", style) 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 container.add_child(tex_rect) else: # Fallback: colored background with card info var bg = ColorRect.new() bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.color = _get_element_color(card.card_data.get_primary_element()) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE container.add_child(bg) var name_label = Label.new() name_label.text = card.card_data.name name_label.position = Vector2(5, 5) name_label.size = Vector2(CARD_WIDTH - 10, 40) name_label.add_theme_font_size_override("font_size", 10) name_label.add_theme_color_override("font_color", Color.BLACK) name_label.autowrap_mode = TextServer.AUTOWRAP_WORD name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE container.add_child(name_label) var cost_label = Label.new() cost_label.text = str(card.card_data.cost) cost_label.position = Vector2(CARD_WIDTH - 25, 5) cost_label.size = Vector2(20, 20) cost_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER cost_label.add_theme_font_size_override("font_size", 14) cost_label.add_theme_color_override("font_color", Color.WHITE) cost_label.mouse_filter = Control.MOUSE_FILTER_IGNORE container.add_child(cost_label) container.gui_input.connect(_on_card_gui_input.bind(index)) container.mouse_entered.connect(_on_card_mouse_entered.bind(index)) container.mouse_exited.connect(_on_card_mouse_exited.bind(index)) return container func _get_element_color(element: Enums.Element) -> Color: var color = Enums.get_element_color(element) return color.lightened(0.4) func _layout_cards() -> void: _position_toggle_button() var card_count = hand_cards.size() if card_count == 0: return var display_width = size.x if display_width <= 0: var viewport = get_viewport() if viewport: display_width = viewport.get_visible_rect().size.x - 100 if display_width <= 0: display_width = 1820 var total_width = CARD_WIDTH + (card_count - 1) * CARD_OVERLAP var start_x = (display_width - total_width) / 2.0 # Base Y position - cards at y=0 in container, hover lifts them up (negative y) var base_y = 0.0 for i in range(card_count): var card_ui = hand_cards[i] var x = start_x + i * CARD_OVERLAP var y = base_y # Lift hovered card slightly (not when minimized) if i == hovered_card_index and selected_card_index == -1 and not is_minimized: y -= HOVER_LIFT # Lift by 15px # Dim selected card in hand if i == selected_card_index: card_ui.modulate = Color(0.5, 0.5, 0.5, 0.7) else: card_ui.modulate = Color.WHITE var center_offset = i - (card_count - 1) / 2.0 var angle = center_offset * FAN_ANGLE card_ui.position = Vector2(x, y) card_ui.rotation = deg_to_rad(angle) card_ui.z_index = i if i == hovered_card_index: card_ui.z_index = 100 func _on_card_gui_input(event: InputEvent, index: int) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: if index >= 0 and index < card_instances.size(): if is_minimized: # Clicking a peeking card expands the hand and selects it is_minimized = false toggle_button.text = "Hide Hand" hand_minimized_changed.emit(false) _layout_cards() _position_toggle_button() _select_card(index) elif selected_card_index == index: _deselect_card() else: _select_card(index) func _on_card_mouse_entered(index: int) -> void: hovered_card_index = index _layout_cards() # Hover signal is no longer used for GameUI preview (selection panel shows card instead) # Keep the signal for potential future use but don't emit it pass func _on_card_mouse_exited(index: int) -> void: if hovered_card_index == index: hovered_card_index = -1 _layout_cards() card_unhovered.emit() func _select_card(index: int) -> void: if index < 0 or index >= card_instances.size(): return selected_card_index = index var card = card_instances[index] # Emit selection signal first (so Main.gd can hide hover preview) card_selected.emit(card) # Also emit unhovered to ensure hover state is cleared card_unhovered.emit() # Clear previous card image from the card container for child in card_image_container.get_children(): child.queue_free() # Clear action menu buttons for child in action_menu.get_children(): child.queue_free() # Add card image to the card container 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 card_image_container.add_child(tex_rect) else: # Fallback colored rect for cards without images var color_rect = ColorRect.new() color_rect.set_anchors_preset(Control.PRESET_FULL_RECT) color_rect.color = _get_element_color(card.card_data.get_primary_element()) card_image_container.add_child(color_rect) var name_label = Label.new() name_label.text = card.card_data.name name_label.position = Vector2(5, 10) name_label.size = Vector2(SELECTED_CARD_WIDTH - 10, 50) name_label.add_theme_font_size_override("font_size", 16) name_label.add_theme_color_override("font_color", Color.BLACK) name_label.autowrap_mode = TextServer.AUTOWRAP_WORD name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER card_image_container.add_child(name_label) # Add action buttons (right side via action_menu VBoxContainer) _add_action_button("Play Card", "play") _add_action_button("Discard for CP", "discard_cp") _add_action_button("View Details", "view") _add_action_button("Cancel", "cancel") # Position the panel centered above the hand var viewport = get_viewport() if viewport: var vp_size = viewport.get_visible_rect().size # Center horizontally relative to screen var panel_x = (vp_size.x - selected_card_panel.size.x) / 2.0 - global_position.x # Position above hand container var panel_y = -selected_card_panel.size.y - 20 # Clamp to stay on screen var global_y = global_position.y + panel_y if global_y < 90: panel_y = 90 - global_position.y var global_x = global_position.x + panel_x if global_x < 10: panel_x = 10 - global_position.x if global_x + selected_card_panel.size.x > vp_size.x - 10: panel_x = vp_size.x - 10 - selected_card_panel.size.x - global_position.x selected_card_panel.position = Vector2(panel_x, panel_y) selected_card_panel.visible = true _layout_cards() func _add_action_button(text: String, action: String) -> void: var button = Button.new() button.text = text button.custom_minimum_size = Vector2(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_action_pressed.bind(action)) action_menu.add_child(button) func _on_action_pressed(action: String) -> void: if selected_card_index < 0 or selected_card_index >= card_instances.size(): _deselect_card() return var card = card_instances[selected_card_index] if action == "cancel": _deselect_card() else: card_action_requested.emit(card, action) if action != "view": _deselect_card() func _deselect_card() -> void: selected_card_index = -1 selected_card_panel.visible = false _layout_cards() ## Public method to deselect (called from Main when action completes) func deselect() -> void: _deselect_card() ## Check if a card is currently selected func has_selection() -> bool: return selected_card_index >= 0 ## Highlight playable cards func highlight_playable(predicate: Callable) -> void: for i in range(hand_cards.size()): var card_ui = hand_cards[i] var card = card_instances[i] var first_child = card_ui.get_child(0) if card_ui.get_child_count() > 0 else null if first_child is TextureRect: if predicate.call(card): first_child.modulate = Color(1.2, 1.2, 1.2) else: first_child.modulate = Color(0.5, 0.5, 0.5) elif first_child is ColorRect: if predicate.call(card): first_child.color = _get_element_color(card.card_data.get_primary_element()).lightened(0.2) else: first_child.color = _get_element_color(card.card_data.get_primary_element()).darkened(0.4) ## Clear all highlights func clear_highlights() -> void: for i in range(hand_cards.size()): var card_ui = hand_cards[i] var card = card_instances[i] var first_child = card_ui.get_child(0) if card_ui.get_child_count() > 0 else null if first_child is TextureRect: first_child.modulate = Color.WHITE elif first_child is ColorRect: first_child.color = _get_element_color(card.card_data.get_primary_element()) func _notification(what: int) -> void: if what == NOTIFICATION_RESIZED: _layout_cards() func _input(event: InputEvent) -> void: # Close selection on Escape if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: if selected_card_index >= 0: _deselect_card() get_viewport().set_input_as_handled()