diff --git a/.claude/settings.local.json b/.claude/settings.local.json index be02d54..75d8b24 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(flatpak list)", "Bash(snap list:*)", "Bash(timeout 10 godot4:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(timeout 5 godot4:*)" ] } } diff --git a/.godot/editor/editor_layout.cfg b/.godot/editor/editor_layout.cfg index a1a1e7f..b42739b 100644 --- a/.godot/editor/editor_layout.cfg +++ b/.godot/editor/editor_layout.cfg @@ -30,14 +30,14 @@ dock_5="Inspector,Node,History" open_scenes=PackedStringArray("res://scenes/main.tscn") current_scene="res://scenes/main.tscn" center_split_offset=0 -selected_default_debugger_tab_idx=1 +selected_default_debugger_tab_idx=0 selected_main_editor_idx=2 -selected_bottom_panel_item=1 +selected_bottom_panel_item=0 [ScriptEditor] -open_scripts=["res://scripts/autoload/CardDatabase.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/Main.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/visual/TableSetup.gd"] -selected_script="res://scripts/ui/HandDisplay.gd" +open_scripts=["res://scripts/ui/ActionLog.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/Main.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/visual/TableSetup.gd", "res://scripts/game/UndoSystem.gd"] +selected_script="res://scripts/ui/ActionLog.gd" open_help=[] script_split_offset=140 list_split_offset=0 diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg index 1fef4d3..954a727 100644 --- a/.godot/editor/project_metadata.cfg +++ b/.godot/editor/project_metadata.cfg @@ -10,7 +10,7 @@ run_reload_scripts=true [recent_files] scenes=["res://scenes/main.tscn"] -scripts=["res://scripts/ui/HandDisplay.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/visual/TableSetup.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/Main.gd"] +scripts=["res://scripts/ui/ActionLog.gd", "res://scripts/game/UndoSystem.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/visual/TableSetup.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/Main.gd"] [linked_properties] diff --git a/.godot/editor/script_editor_cache.cfg b/.godot/editor/script_editor_cache.cfg index abfe02e..ab2947a 100644 --- a/.godot/editor/script_editor_cache.cfg +++ b/.godot/editor/script_editor_cache.cfg @@ -6,7 +6,7 @@ state={ "column": 0, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 68, +"row": 75, "scroll_position": 64.0, "selection": false, "syntax_highlighter": "GDScript" @@ -63,7 +63,35 @@ state={ "folded_lines": Array[int]([]), "h_scroll_position": 0, "row": 166, -"scroll_position": 166.0, +"scroll_position": 161.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/game/UndoSystem.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 71, +"scroll_position": 71.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/ui/ActionLog.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 225, +"scroll_position": 220.0, "selection": false, "syntax_highlighter": "GDScript" } diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg index 093aa84..d98abc9 100644 --- a/.godot/global_script_class_cache.cfg +++ b/.godot/global_script_class_cache.cfg @@ -1,4 +1,10 @@ list=Array[Dictionary]([{ +"base": &"Control", +"class": &"ActionLog", +"icon": "", +"language": &"GDScript", +"path": "res://scripts/ui/ActionLog.gd" +}, { "base": &"RefCounted", "class": &"CPPool", "icon": "", @@ -84,6 +90,12 @@ list=Array[Dictionary]([{ "path": "res://scripts/game/TurnManager.gd" }, { "base": &"RefCounted", +"class": &"UndoSystem", +"icon": "", +"language": &"GDScript", +"path": "res://scripts/game/UndoSystem.gd" +}, { +"base": &"RefCounted", "class": &"Zone", "icon": "", "language": &"GDScript", diff --git a/scripts/Main.gd b/scripts/Main.gd index 79369df..7e4fcde 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -7,6 +7,7 @@ var table_setup: TableSetup var game_ui: GameUI var hand_display: HandDisplay var hand_layer: CanvasLayer +var action_log: ActionLog # Player damage displays var damage_displays: Array[DamageDisplay] = [] @@ -35,6 +36,29 @@ func _setup_ui() -> void: game_ui.layer = 10 # Base UI layer add_child(game_ui) + # Action log (collapsible panel on right side) + var log_layer = CanvasLayer.new() + log_layer.layer = 12 # Above other UI + add_child(log_layer) + + action_log = ActionLog.new() + log_layer.add_child(action_log) + # Position on right side of screen + action_log.set_anchors_preset(Control.PRESET_CENTER_RIGHT) + action_log.offset_left = -290 + action_log.offset_right = -10 + action_log.offset_top = -200 + action_log.offset_bottom = 200 + + # Connect undo signal + action_log.undo_requested.connect(_on_undo_requested) + + # Connect to GameManager undo signals + GameManager.undo_available_changed.connect(_on_undo_available_changed) + + # Ensure ActionLog connects to GameManager after it's in the tree + action_log.call_deferred("_connect_game_manager_signals") + # Hand display needs its own CanvasLayer to render on top of 3D hand_layer = CanvasLayer.new() hand_layer.layer = 11 # Above the game UI @@ -71,6 +95,18 @@ func _position_hand_display() -> void: hand_display.position = Vector2(50, vp_size.y - 180) hand_display.size = Vector2(vp_size.x - 100, 170) + # Connect to viewport size changed signal if not already connected + if not viewport.size_changed.is_connected(_on_viewport_resized): + viewport.size_changed.connect(_on_viewport_resized) + +func _on_viewport_resized() -> void: + # Reposition hand display when window resizes + var viewport = get_viewport() + if viewport and hand_display: + var vp_size = viewport.get_visible_rect().size + hand_display.position = Vector2(50, vp_size.y - 180) + hand_display.size = Vector2(vp_size.x - 100, 170) + func _connect_signals() -> void: # GameManager signals GameManager.game_started.connect(_on_game_started) @@ -157,12 +193,10 @@ func _update_playable_highlights() -> void: hand_display.clear_highlights() func _on_hand_card_selected(card: CardInstance) -> void: - print("Main: Card selected: ", card.card_data.name, " input_mode=", GameManager.input_mode) var input_mode = GameManager.input_mode match input_mode: GameManager.InputMode.SELECT_CARD_TO_PLAY: - print("Main: Trying to play card") GameManager.try_play_card(card) _sync_visuals() _update_hand_display() @@ -170,12 +204,9 @@ func _on_hand_card_selected(card: CardInstance) -> void: GameManager.InputMode.SELECT_CP_SOURCE: # Discard for CP - print("Main: Discarding for CP") GameManager.discard_card_for_cp(card) _update_hand_display() _update_cp_display() - _: - print("Main: Input mode not handled: ", input_mode) func _on_hand_card_hovered(card: CardInstance) -> void: game_ui.show_card_detail(card) @@ -183,6 +214,19 @@ func _on_hand_card_hovered(card: CardInstance) -> void: func _on_hand_card_unhovered() -> void: game_ui.hide_card_detail() +func _on_undo_requested() -> void: + if GameManager.undo_last_action(): + _sync_visuals() + _update_hand_display() + _update_cp_display() + _update_playable_highlights() + # Restore input mode based on current phase + GameManager.restore_input_mode_for_phase() + +func _on_undo_available_changed(available: bool) -> void: + if action_log: + action_log.set_undo_available(available) + func _on_table_card_clicked(card: CardInstance, zone_type: Enums.ZoneType, player_index: int) -> void: var input_mode = GameManager.input_mode @@ -216,6 +260,17 @@ func _on_table_card_clicked(card: CardInstance, zone_type: Enums.ZoneType, playe func _input(event: InputEvent) -> void: # Keyboard shortcuts if event is InputEventKey and event.pressed: + # Ctrl+Z for undo + if event.keycode == KEY_Z and event.ctrl_pressed: + _on_undo_requested() + return + + # L key to toggle action log + if event.keycode == KEY_L and not event.ctrl_pressed: + if action_log: + action_log.toggle_panel() + return + match event.keycode: KEY_SPACE: # Pass priority / end phase diff --git a/scripts/autoload/GameManager.gd b/scripts/autoload/GameManager.gd index 3eeeace..cdde3c1 100644 --- a/scripts/autoload/GameManager.gd +++ b/scripts/autoload/GameManager.gd @@ -11,10 +11,15 @@ signal phase_changed(phase_name: String) signal card_played(card_data: Dictionary) signal damage_dealt(player_name: String, amount: int) signal message(text: String) +signal action_undone(action_name: String) +signal undo_available_changed(available: bool) # Game state var game_state: GameState = null +# Undo system +var undo_system: UndoSystem = null + # State flags var is_initialized: bool = false var is_game_active: bool = false @@ -55,6 +60,11 @@ func start_new_game() -> void: # Create new game state game_state = GameState.new() + # Create undo system + undo_system = UndoSystem.new(game_state) + undo_system.undo_available_changed.connect(_on_undo_available_changed) + undo_system.action_undone.connect(_on_action_undone) + # Connect signals _connect_game_signals() @@ -131,21 +141,67 @@ func try_play_card(card: CardInstance) -> bool: selected_card = card return false + # Check play restrictions before attempting to play + var play_error = _check_play_restrictions(player, card) + if play_error != "": + message.emit(play_error) + return false + + # Determine target zone + var to_zone = Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS + # Try to play if game_state.play_card(player_index, card): + # Record for undo + if undo_system: + undo_system.record_play_card(player_index, card, to_zone, {}) + message.emit("Played " + card.get_display_name()) return true else: - message.emit("Cannot play that card!") + # This shouldn't happen if _check_play_restrictions works correctly + # but provide context if it does + message.emit("Failed to play " + card.get_display_name() + " (internal error)") return false +## Check play restrictions and return error message, or empty string if playable +func _check_play_restrictions(player: Player, card: CardInstance) -> String: + # Check if card is in hand + if not player.hand.has_card(card): + return "Card is no longer in your hand!" + + # Check backup limit + if card.is_backup(): + if player.field_backups.get_count() >= Player.MAX_BACKUPS: + return "Cannot play: Maximum 5 Backups allowed!" + + # Check unique name restriction (non-generic cards) + if not card.card_data.is_generic: + if card.is_forward() and player.field_forwards.has_card_with_name(card.card_data.name): + return "Cannot play: You already have " + card.card_data.name + " on the field!" + if card.is_backup() and player.field_backups.has_card_with_name(card.card_data.name): + return "Cannot play: You already have " + card.card_data.name + " on the field!" + + # Check Light/Dark restriction + if card.is_light_or_dark(): + if player.field_forwards.has_light_or_dark() or player.field_backups.has_light_or_dark(): + return "Cannot play: You can only have one Light/Dark card on the field!" + + return "" + ## Discard a card to generate CP func discard_card_for_cp(card: CardInstance) -> bool: if not game_state or not is_game_active: return false var player_index = card.owner_index + var element = card.get_element() + if game_state.discard_for_cp(player_index, card): + # Record for undo + if undo_system: + undo_system.record_discard_for_cp(player_index, card, element) + message.emit("Discarded " + card.get_display_name() + " for 2 CP") _check_pending_action() return true @@ -158,7 +214,13 @@ func dull_backup_for_cp(card: CardInstance) -> bool: return false var player_index = card.controller_index + var element = card.get_element() + if game_state.dull_backup_for_cp(player_index, card): + # Record for undo + if undo_system: + undo_system.record_dull_backup_for_cp(player_index, card, element) + message.emit("Dulled " + card.get_display_name() + " for 1 CP") _check_pending_action() return true @@ -169,6 +231,13 @@ func dull_backup_for_cp(card: CardInstance) -> bool: func _check_pending_action() -> void: if input_mode == InputMode.SELECT_CP_SOURCE and selected_card: var player = game_state.get_player(selected_card.controller_index) + + # Check if the card is still in hand (it may have been discarded for CP) + if not player.hand.has_card(selected_card): + message.emit("The card you wanted to play was discarded!") + clear_selection() + return + if player.cp_pool.can_afford_card(selected_card.card_data): # Can now afford - try to play try_play_card(selected_card) @@ -233,8 +302,48 @@ func clear_selection() -> void: selected_card = null pending_action = func(): pass +## Undo the last action +func undo_last_action() -> bool: + if not undo_system: + return false + + return undo_system.undo() + +## Check if undo is available +func can_undo() -> bool: + if not undo_system: + return false + return undo_system.can_undo() + +## Get description of last undoable action +func get_undo_description() -> String: + if not undo_system: + return "" + return undo_system.get_last_action_description() + +## Restore the input mode based on the current phase +func restore_input_mode_for_phase() -> void: + if not game_state: + return + + var phase = game_state.turn_manager.current_phase + match phase: + Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2: + input_mode = InputMode.SELECT_CARD_TO_PLAY + Enums.TurnPhase.ATTACK: + input_mode = InputMode.SELECT_ATTACKER + _: + input_mode = InputMode.NONE + ## Signal handlers +func _on_undo_available_changed(available: bool) -> void: + undo_available_changed.emit(available) + +func _on_action_undone(action_name: String) -> void: + message.emit("Undid: " + action_name) + action_undone.emit(action_name) + func _on_game_ended(winner_index: int) -> void: is_game_active = false var winner_name = game_state.get_player(winner_index).player_name @@ -276,6 +385,10 @@ func _on_phase_changed(phase: Enums.TurnPhase) -> void: var phase_name = Enums.phase_to_string(phase) phase_changed.emit(phase_name) + # Clear undo history on phase change + if undo_system: + undo_system.clear_history() + # Set appropriate input mode match phase: Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2: diff --git a/scripts/game/TurnManager.gd b/scripts/game/TurnManager.gd index 101079a..7d8a3ad 100644 --- a/scripts/game/TurnManager.gd +++ b/scripts/game/TurnManager.gd @@ -28,6 +28,8 @@ func start_game(first_player: int) -> void: is_first_turn = true current_phase = Enums.TurnPhase.ACTIVE turn_started.emit(current_player_index, turn_number) + # Emit phase_changed to trigger the initial Active phase execution + phase_changed.emit(current_phase) ## Advance to the next phase func advance_phase() -> Enums.TurnPhase: diff --git a/scripts/game/UndoSystem.gd b/scripts/game/UndoSystem.gd new file mode 100644 index 0000000..d31d1eb --- /dev/null +++ b/scripts/game/UndoSystem.gd @@ -0,0 +1,193 @@ +class_name UndoSystem +extends RefCounted + +## UndoSystem - Tracks the last action for undo capability + +signal undo_available_changed(available: bool) +signal action_undone(action_name: String) + +# Action types that can be undone +enum ActionType { + NONE, + DISCARD_FOR_CP, + DULL_BACKUP_FOR_CP, + PLAY_CARD +} + +# Stored action data for undo +class UndoAction: + var type: ActionType = ActionType.NONE + var player_index: int = -1 + var card: CardInstance = null + var description: String = "" + # Additional data depending on action type + var cp_element: Enums.Element = Enums.Element.FIRE + var cp_amount: int = 0 + var previous_card_state: Enums.CardState = Enums.CardState.ACTIVE + var from_zone: Enums.ZoneType = Enums.ZoneType.HAND + var to_zone: Enums.ZoneType = Enums.ZoneType.FIELD_FORWARDS + +var last_action: UndoAction = null +var game_state: GameState = null + +func _init(state: GameState = null) -> void: + game_state = state + +## Set the game state reference +func set_game_state(state: GameState) -> void: + game_state = state + +## Check if undo is available +func can_undo() -> bool: + return last_action != null and last_action.type != ActionType.NONE + +## Record a discard for CP action +func record_discard_for_cp(player_index: int, card: CardInstance, element: Enums.Element) -> void: + var action = UndoAction.new() + action.type = ActionType.DISCARD_FOR_CP + action.player_index = player_index + action.card = card + action.cp_element = element + action.cp_amount = 2 + action.description = "Discard " + card.get_display_name() + " for CP" + + last_action = action + undo_available_changed.emit(true) + +## Record a dull backup for CP action +func record_dull_backup_for_cp(player_index: int, card: CardInstance, element: Enums.Element) -> void: + var action = UndoAction.new() + action.type = ActionType.DULL_BACKUP_FOR_CP + action.player_index = player_index + action.card = card + action.cp_element = element + action.cp_amount = 1 + action.previous_card_state = Enums.CardState.ACTIVE # Was active before dulling + action.description = "Dull " + card.get_display_name() + " for CP" + + last_action = action + undo_available_changed.emit(true) + +## Record playing a card +func record_play_card(player_index: int, card: CardInstance, to_zone: Enums.ZoneType, _cp_spent: Dictionary) -> void: + var action = UndoAction.new() + action.type = ActionType.PLAY_CARD + action.player_index = player_index + action.card = card + action.from_zone = Enums.ZoneType.HAND + action.to_zone = to_zone + action.description = "Play " + card.get_display_name() + + last_action = action + undo_available_changed.emit(true) + +## Clear the undo history (called when phase changes, combat happens, etc.) +func clear_history() -> void: + last_action = null + undo_available_changed.emit(false) + +## Execute undo of the last action +func undo() -> bool: + if not can_undo() or not game_state: + return false + + var action = last_action + var success = false + + match action.type: + ActionType.DISCARD_FOR_CP: + success = _undo_discard_for_cp(action) + ActionType.DULL_BACKUP_FOR_CP: + success = _undo_dull_backup_for_cp(action) + ActionType.PLAY_CARD: + success = _undo_play_card(action) + + if success: + var description = action.description + last_action = null + undo_available_changed.emit(false) + action_undone.emit(description) + + return success + +## Undo a discard for CP action +func _undo_discard_for_cp(action: UndoAction) -> bool: + var player = game_state.get_player(action.player_index) + if not player: + return false + + # Remove card from break zone + if not player.break_zone.has_card(action.card): + return false + + player.break_zone.remove_card(action.card) + + # Add card back to hand + player.hand.add_card(action.card) + + # Remove the CP that was generated + player.cp_pool.add_cp(action.cp_element, -action.cp_amount) + + return true + +## Undo a dull backup for CP action +func _undo_dull_backup_for_cp(action: UndoAction) -> bool: + var player = game_state.get_player(action.player_index) + if not player: + return false + + # Check card is still on field + if not player.field_backups.has_card(action.card): + return false + + # Reactivate the backup + action.card.activate() + + # Remove the CP that was generated + player.cp_pool.add_cp(action.cp_element, -action.cp_amount) + + return true + +## Undo playing a card +func _undo_play_card(action: UndoAction) -> bool: + var player = game_state.get_player(action.player_index) + if not player: + return false + + var card = action.card + + # Remove from field + var removed = false + if action.to_zone == Enums.ZoneType.FIELD_FORWARDS: + if player.field_forwards.has_card(card): + player.field_forwards.remove_card(card) + removed = true + elif action.to_zone == Enums.ZoneType.FIELD_BACKUPS: + if player.field_backups.has_card(card): + player.field_backups.remove_card(card) + removed = true + + if not removed: + return false + + # Return to hand + player.hand.add_card(card) + + # Reset card state + card.state = Enums.CardState.ACTIVE + card.turns_on_field = 0 + + # Refund the CP (we don't track exact CP spent, so this is simplified) + # In a full implementation, we'd track what CP was spent + # For now, we'll add back CP equal to the card cost + var cost = card.card_data.cost + var element = card.get_element() + player.cp_pool.add_cp(element, cost) + + return true + +## Get description of the last action (for display) +func get_last_action_description() -> String: + if not can_undo(): + return "" + return last_action.description diff --git a/scripts/ui/ActionLog.gd b/scripts/ui/ActionLog.gd new file mode 100644 index 0000000..74c3f97 --- /dev/null +++ b/scripts/ui/ActionLog.gd @@ -0,0 +1,264 @@ +class_name ActionLog +extends Control + +## ActionLog - Collapsible panel showing game actions + +signal undo_requested + +# UI Components +var panel: PanelContainer +var toggle_button: Button +var scroll_container: ScrollContainer +var log_container: VBoxContainer +var undo_button: Button +var clear_button: Button + +# State +var is_expanded: bool = true +var action_entries: Array[Control] = [] + +# Layout constants +const COLLAPSED_WIDTH: float = 40.0 +const EXPANDED_WIDTH: float = 280.0 +const PANEL_HEIGHT: float = 400.0 + +func _ready() -> void: + _create_ui() + _connect_signals() + +func _create_ui() -> void: + # Set up this control to anchor to right side + set_anchors_preset(Control.PRESET_CENTER_RIGHT) + custom_minimum_size = Vector2(EXPANDED_WIDTH, PANEL_HEIGHT) + size = Vector2(EXPANDED_WIDTH, PANEL_HEIGHT) + mouse_filter = Control.MOUSE_FILTER_IGNORE + + # Main horizontal container + var hbox = HBoxContainer.new() + add_child(hbox) + hbox.set_anchors_preset(Control.PRESET_FULL_RECT) + hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL + hbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + + # Toggle button on the left edge + toggle_button = Button.new() + toggle_button.text = ">" + toggle_button.custom_minimum_size = Vector2(30, 0) + toggle_button.size_flags_vertical = Control.SIZE_EXPAND_FILL + toggle_button.tooltip_text = "Toggle Action Log (L)" + hbox.add_child(toggle_button) + + # Panel container for the log + panel = PanelContainer.new() + hbox.add_child(panel) + panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.size_flags_vertical = Control.SIZE_EXPAND_FILL + + var style = StyleBoxFlat.new() + style.bg_color = Color(0.08, 0.08, 0.12, 0.95) + 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) + + # Panel content + var panel_vbox = VBoxContainer.new() + panel.add_child(panel_vbox) + panel_vbox.add_theme_constant_override("separation", 5) + + # Header + var header = HBoxContainer.new() + panel_vbox.add_child(header) + header.add_theme_constant_override("separation", 5) + + var title = Label.new() + title.text = "Action Log" + title.add_theme_font_size_override("font_size", 14) + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(title) + + # Undo button + undo_button = Button.new() + undo_button.text = "Undo" + undo_button.custom_minimum_size = Vector2(50, 25) + undo_button.add_theme_font_size_override("font_size", 11) + undo_button.tooltip_text = "Undo last action (Ctrl+Z)" + undo_button.disabled = true + header.add_child(undo_button) + + # Clear button + clear_button = Button.new() + clear_button.text = "Clear" + clear_button.custom_minimum_size = Vector2(45, 25) + clear_button.add_theme_font_size_override("font_size", 11) + header.add_child(clear_button) + + # Separator + var separator = HSeparator.new() + panel_vbox.add_child(separator) + + # Scroll container for log entries + scroll_container = ScrollContainer.new() + panel_vbox.add_child(scroll_container) + scroll_container.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll_container.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + scroll_container.custom_minimum_size = Vector2(200, 250) + + # Log entries container + log_container = VBoxContainer.new() + scroll_container.add_child(log_container) + log_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + log_container.size_flags_vertical = Control.SIZE_EXPAND_FILL + log_container.add_theme_constant_override("separation", 3) + +func _connect_signals() -> void: + toggle_button.pressed.connect(_on_toggle_pressed) + undo_button.pressed.connect(_on_undo_pressed) + clear_button.pressed.connect(clear_log) + + # Defer GameManager connections to ensure it's ready + call_deferred("_connect_game_manager_signals") + +func _connect_game_manager_signals() -> void: + # Connect to GameManager signals + if GameManager: + if not GameManager.message.is_connected(_on_game_message): + GameManager.message.connect(_on_game_message) + if not GameManager.card_played.is_connected(_on_card_played): + GameManager.card_played.connect(_on_card_played) + if not GameManager.turn_changed.is_connected(_on_turn_changed): + GameManager.turn_changed.connect(_on_turn_changed) + if not GameManager.phase_changed.is_connected(_on_phase_changed): + GameManager.phase_changed.connect(_on_phase_changed) + if not GameManager.damage_dealt.is_connected(_on_damage_dealt): + GameManager.damage_dealt.connect(_on_damage_dealt) + if not GameManager.action_undone.is_connected(_on_action_undone): + GameManager.action_undone.connect(_on_action_undone) + +func _on_toggle_pressed() -> void: + toggle_panel() + +func toggle_panel() -> void: + is_expanded = not is_expanded + panel.visible = is_expanded + toggle_button.text = ">" if is_expanded else "<" + + if is_expanded: + custom_minimum_size.x = EXPANDED_WIDTH + size.x = EXPANDED_WIDTH + else: + custom_minimum_size.x = COLLAPSED_WIDTH + size.x = COLLAPSED_WIDTH + +func _on_undo_pressed() -> void: + undo_requested.emit() + +## Add a log entry with a specific type for styling +func add_entry(text: String, entry_type: String = "info") -> void: + if not log_container: + return + + var entry = _create_entry(text, entry_type) + log_container.add_child(entry) + action_entries.append(entry) + + # Keep log at reasonable size + while action_entries.size() > 100: + var old_entry = action_entries.pop_front() + old_entry.queue_free() + + # Scroll to bottom after adding + call_deferred("_scroll_to_bottom") + +func _create_entry(text: String, entry_type: String) -> Control: + var entry_panel = PanelContainer.new() + + var style = StyleBoxFlat.new() + style.set_content_margin_all(4) + style.set_corner_radius_all(3) + + # Color based on type + match entry_type: + "turn": + style.bg_color = Color(0.2, 0.3, 0.4, 0.8) + "phase": + style.bg_color = Color(0.15, 0.25, 0.35, 0.6) + "action": + style.bg_color = Color(0.2, 0.2, 0.3, 0.7) + "damage": + style.bg_color = Color(0.4, 0.15, 0.15, 0.7) + "card": + style.bg_color = Color(0.15, 0.3, 0.2, 0.7) + _: + style.bg_color = Color(0.12, 0.12, 0.18, 0.5) + + entry_panel.add_theme_stylebox_override("panel", style) + + var label = Label.new() + label.text = text + label.add_theme_font_size_override("font_size", 11) + label.autowrap_mode = TextServer.AUTOWRAP_WORD + label.custom_minimum_size.x = 200 + entry_panel.add_child(label) + + return entry_panel + +func _scroll_to_bottom() -> void: + await get_tree().process_frame + scroll_container.scroll_vertical = int(scroll_container.get_v_scroll_bar().max_value) + +func clear_log() -> void: + for entry in action_entries: + entry.queue_free() + action_entries.clear() + +## Enable or disable undo button +func set_undo_available(available: bool) -> void: + undo_button.disabled = not available + +## Manually add a message (can be called from Main.gd if signal connection fails) +func log_message(text: String) -> void: + _on_game_message(text) + +## Manually log a turn change +func log_turn_change(player_name: String, turn_number: int) -> void: + _on_turn_changed(player_name, turn_number) + +## Manually log a phase change +func log_phase_change(phase_name: String) -> void: + _on_phase_changed(phase_name) + +## Signal handlers +func _on_game_message(text: String) -> void: + # Determine entry type based on message content + var entry_type = "info" + if "attacks" in text or "blocks" in text: + entry_type = "action" + elif "damage" in text or "broken" in text: + entry_type = "damage" + elif "Played" in text or "Discarded" in text or "Dulled" in text: + entry_type = "card" + + add_entry(text, entry_type) + +func _on_card_played(_card_data: Dictionary) -> void: + # Already handled by message signal + pass + +func _on_turn_changed(player_name: String, turn_number: int) -> void: + add_entry("=== " + player_name + " - Turn " + str(turn_number) + " ===", "turn") + +func _on_phase_changed(phase_name: String) -> void: + add_entry(phase_name, "phase") + +func _on_damage_dealt(_player_name: String, _amount: int) -> void: + # Already handled by message signal + pass + +func _on_action_undone(action_name: String) -> void: + add_entry("UNDO: " + action_name, "action") + +# Input is handled by Main.gd to avoid duplicate handling diff --git a/scripts/ui/GameUI.gd b/scripts/ui/GameUI.gd index 5066ae0..7724288 100644 --- a/scripts/ui/GameUI.gd +++ b/scripts/ui/GameUI.gd @@ -126,17 +126,29 @@ func _create_ui() -> void: end_phase_button.custom_minimum_size = Vector2(100, 40) action_buttons.add_child(end_phase_button) - # === MESSAGE PANEL (center, above hand) === + # === 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() - root.add_child(message_panel) - message_panel.set_anchors_preset(Control.PRESET_CENTER) - message_panel.position = Vector2(-100, 200) - message_panel.custom_minimum_size = Vector2(200, 40) + 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", 16) + 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 diff --git a/scripts/ui/HandDisplay.gd b/scripts/ui/HandDisplay.gd index b4ae52c..0ec4f22 100644 --- a/scripts/ui/HandDisplay.gd +++ b/scripts/ui/HandDisplay.gd @@ -191,9 +191,7 @@ func _layout_cards() -> void: func _on_card_gui_input(event: InputEvent, index: int) -> void: if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: - print("HandDisplay: Card clicked, index=", index) if index >= 0 and index < card_instances.size(): - print("HandDisplay: Emitting card_selected for ", card_instances[index].card_data.name) card_selected.emit(card_instances[index]) func _on_card_mouse_entered(index: int) -> void: