Files
FFCardGame/scripts/ui/GameUI.gd
2026-02-02 16:28:53 -05:00

605 lines
19 KiB
GDScript

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()
## Hide message immediately (e.g., when AI finishes thinking)
func hide_message() -> void:
message_queue.clear()
message_panel.visible = false
message_timer.stop()
## 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