diff --git a/.godot/editor/editor_layout.cfg b/.godot/editor/editor_layout.cfg index f303405..6164955 100644 --- a/.godot/editor/editor_layout.cfg +++ b/.godot/editor/editor_layout.cfg @@ -19,8 +19,8 @@ dock_filesystem_split=0 dock_filesystem_display_mode=0 dock_filesystem_file_sort=0 dock_filesystem_file_list_display_mode=1 -dock_filesystem_selected_paths=PackedStringArray("res://scripts/visual/TableCamera.gd") -dock_filesystem_uncollapsed_paths=PackedStringArray("Favorites", "res://", "res://scripts/", "res://scripts/visual/") +dock_filesystem_selected_paths=PackedStringArray("res://scripts/ui/GameUI.gd") +dock_filesystem_uncollapsed_paths=PackedStringArray("Favorites", "res://", "res://scripts/", "res://scripts/visual/", "res://scripts/ui/") dock_3="Scene,Import" dock_4="FileSystem" dock_5="Inspector,Node,History" @@ -36,8 +36,8 @@ selected_bottom_panel_item=0 [ScriptEditor] -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/TableCamera.gd", "res://scripts/visual/TableSetup.gd", "res://scripts/game/UndoSystem.gd"] -selected_script="res://scripts/visual/TableCamera.gd" +open_scripts=["res://scripts/ui/ActionLog.gd", "res://scripts/autoload/CardDatabase.gd", "res://scripts/game/CardInstance.gd", "res://scripts/game/CPPool.gd", "res://scripts/game/GameState.gd", "res://scripts/ui/GameUI.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/Main.gd", "res://scripts/ui/PauseMenu.gd", "res://scripts/game/Player.gd", "res://scripts/visual/TableCamera.gd", "res://scripts/visual/TableSetup.gd", "res://scripts/game/UndoSystem.gd"] +selected_script="res://scripts/ui/GameUI.gd" open_help=[] script_split_offset=140 list_split_offset=0 diff --git a/.godot/editor/filesystem_cache8 b/.godot/editor/filesystem_cache8 index 918a88e..6ed5be2 100644 --- a/.godot/editor/filesystem_cache8 +++ b/.godot/editor/filesystem_cache8 @@ -1,17 +1,19 @@ ea4bc82a6ad023ab7ee23ee620429895 -::res://::1769464020 +::res://::1769470366 background_1.png::CompressedTexture2D::259206091835802070::1769464008::1769464212::1::::<><>:: +card_back.png::CompressedTexture2D::4833498016096001590::1769466370::1769466517::1::::<><>:: FF14_Playmat__12516.webp::CompressedTexture2D::1641665221299209414::1769277769::1769280957::1::::<><>:: FF_mat_option_1.png::CompressedTexture2D::4359709237641823626::1769451897::1769453057::1::::<><>:: README.md::TextFile::-1::1769279531::0::1::::<><>:: Screenshot 2026-01-24 at 12-53-03 Untitled-3 - fftcgrulesheet-en.pdf.png::CompressedTexture2D::5958662832102035034::1769277183::1769280957::1::::<><>:: ::res://assets/::1769279430 -::res://assets/cards/::1769280956 +::res://assets/cards/::1769466517 1-003C_eg.jpg::CompressedTexture2D::3078340571116611252::1769279471::1769280956::1::::<><>:: 1-005R_eg.jpg::CompressedTexture2D::9030396388734102056::1769279471::1769280956::1::::<><>:: 1-006H_eg.jpg::CompressedTexture2D::8795536954934893861::1769279471::1769280956::1::::<><>:: 1-007R_eg.jpg::CompressedTexture2D::6933100492479484556::1769279471::1769280956::1::::<><>:: -::res://assets/table/::1769464097 +card_back.png::CompressedTexture2D::7787125851359297441::1769466418::1769466517::1::::<><>:: +::res://assets/table/::1769464212 background_1.png::CompressedTexture2D::102728058489724503::1769464097::1769464212::1::::<><>:: playmat.webp::CompressedTexture2D::3235866490631872101::1769279471::1769280957::1::::<><>:: ::res://assets/ui/::1769280956 @@ -21,9 +23,9 @@ cards.json::JSON::-1::1769309289::0::1::::<><>:: ::res://docs/::1769279608 CARD_FORMAT.md::TextFile::-1::1769279608::0::1::::<><>:: DESIGN.md::TextFile::-1::1769279572::0::1::::<><>:: -::res://scenes/::1769461610 +::res://scenes/::1769466609 game_controller.tscn::PackedScene::3882700613993784342::1769285267::0::1::::<><>::res://scripts/GameController.gd -main.tscn::PackedScene::5942992277112036945::1769461610::0::1::::<><>::res://scripts/Main.gd +main.tscn::PackedScene::5942992277112036945::1769466609::0::1::::<><>::res://scripts/Main.gd ::res://scenes/card/::1769279430 ::res://scenes/main/::1769279430 ::res://scenes/table/::1769279430 @@ -50,11 +52,11 @@ GameUI.gd::GDScript::-1::1769379370::0::1::::GameUI<>CanvasLayer<>:: HandDisplay.gd::GDScript::-1::1769381830::0::1::::HandDisplay<>Control<>:: MainMenu.gd::GDScript::-1::1769285226::0::1::::MainMenu<>CanvasLayer<>:: PauseMenu.gd::GDScript::-1::1769287615::0::1::::PauseMenu<>CanvasLayer<>:: -::res://scripts/visual/::1769464132 +::res://scripts/visual/::1769466440 CardVisual.gd::GDScript::-1::1769460118::0::1::::CardVisual<>Node3D<>:: PlaymatRenderer.gd::GDScript::-1::1769452774::0::1::::PlaymatRenderer<>Node<>:: TableCamera.gd::GDScript::-1::1769461608::0::1::::TableCamera<>Camera3D<>:: -TableSetup.gd::GDScript::-1::1769464132::0::1::::TableSetup<>Node3D<>:: +TableSetup.gd::GDScript::-1::1769466440::0::1::::TableSetup<>Node3D<>:: ZoneVisual.gd::GDScript::-1::1769454229::0::1::::ZoneVisual<>Node3D<>:: ::res://source-cards/::1769308626 1-001H.jpg::CompressedTexture2D::2056726104879484109::1769306016::1769308625::1::::<><>:: diff --git a/.godot/editor/filesystem_update4 b/.godot/editor/filesystem_update4 index 7d65f94..14e1d06 100644 --- a/.godot/editor/filesystem_update4 +++ b/.godot/editor/filesystem_update4 @@ -1,2 +1 @@ res://scenes/main.tscn -res://scripts/visual/TableCamera.gd diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg index 9c53079..514fa48 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/visual/TableCamera.gd", "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"] +scripts=["res://scripts/ui/GameUI.gd", "res://scripts/game/Player.gd", "res://scripts/game/CardInstance.gd", "res://scripts/game/CPPool.gd", "res://scripts/game/GameState.gd", "res://scripts/visual/TableCamera.gd", "res://scripts/ui/ActionLog.gd", "res://scripts/game/UndoSystem.gd", "res://scripts/ui/HandDisplay.gd", "res://scripts/ui/PauseMenu.gd"] [linked_properties] diff --git a/.godot/editor/script_editor_cache.cfg b/.godot/editor/script_editor_cache.cfg index eb81125..1305fa5 100644 --- a/.godot/editor/script_editor_cache.cfg +++ b/.godot/editor/script_editor_cache.cfg @@ -3,11 +3,11 @@ state={ "bookmarks": PackedInt32Array(), "breakpoints": PackedInt32Array(), -"column": 29, +"column": 0, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 49, -"scroll_position": 45.0, +"row": 54, +"scroll_position": 50.0, "selection": false, "syntax_highlighter": "GDScript" } @@ -105,7 +105,77 @@ state={ "folded_lines": Array[int]([]), "h_scroll_position": 0, "row": 8, -"scroll_position": 3.0, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/game/GameState.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 0, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/game/CPPool.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 31, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 3, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/game/CardInstance.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 0, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/game/Player.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 0, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} + +[res://scripts/ui/GameUI.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 0, +"scroll_position": 0.0, "selection": false, "syntax_highlighter": "GDScript" } diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin index 58792a9..72dcae3 100644 Binary files a/.godot/uid_cache.bin and b/.godot/uid_cache.bin differ diff --git a/assets/cards/card_back.png b/assets/cards/card_back.png new file mode 100644 index 0000000..e0f6dc0 Binary files /dev/null and b/assets/cards/card_back.png differ diff --git a/assets/cards/card_back.png.import b/assets/cards/card_back.png.import new file mode 100644 index 0000000..062a148 --- /dev/null +++ b/assets/cards/card_back.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di7e3b1dpuo1f" +path="res://.godot/imported/card_back.png-2577f00fd56181f44075a936080e7577.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/cards/card_back.png" +dest_files=["res://.godot/imported/card_back.png-2577f00fd56181f44075a936080e7577.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/card_back.png b/card_back.png new file mode 100644 index 0000000..e0f6dc0 Binary files /dev/null and b/card_back.png differ diff --git a/card_back.png.import b/card_back.png.import new file mode 100644 index 0000000..bb4e251 --- /dev/null +++ b/card_back.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ca4nahdobuqyw" +path="res://.godot/imported/card_back.png-0c38304b68d8509854fe3cd66366fdbb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://card_back.png" +dest_files=["res://.godot/imported/card_back.png-0c38304b68d8509854fe3cd66366fdbb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scripts/Main.gd b/scripts/Main.gd index c46c6d2..4b536d9 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -123,6 +123,9 @@ func _connect_signals() -> void: GameManager.phase_changed.connect(_on_phase_changed) GameManager.damage_dealt.connect(_on_damage_dealt) + # Field card action signal (deferred to ensure game_ui is ready) + call_deferred("_connect_field_card_signals") + func _start_game() -> void: GameManager.start_new_game() # Force an update of visuals after a frame to ensure everything is ready @@ -137,6 +140,11 @@ func _on_game_started() -> void: _sync_visuals() _update_hand_display() + # Set initial camera to first player's perspective + if GameManager.game_state and table_setup: + var player_index = GameManager.game_state.turn_manager.current_player_index + table_setup.switch_camera_to_player(player_index) + func _on_game_ended(winner_name: String) -> void: game_ui.show_message(winner_name + " wins the game!") @@ -145,9 +153,16 @@ func _on_turn_changed(_player_name: String, _turn_number: int) -> void: _update_hand_display() _update_cp_display() + # Rotate camera to face the current player's mat + if GameManager.game_state and table_setup: + var player_index = GameManager.game_state.turn_manager.current_player_index + table_setup.switch_camera_to_player(player_index) + func _on_phase_changed(_phase_name: String) -> void: _update_playable_highlights() _update_cp_display() + # Refresh hand after draw phase completes (hand updates on entering main phase) + _update_hand_display() func _on_damage_dealt(player_name: String, _amount: int) -> void: # Find player index @@ -229,7 +244,9 @@ func _on_hand_card_unhovered() -> void: game_ui.hide_card_detail() func _on_hand_card_selected(_card: CardInstance) -> void: - # Selection panel is now visible - ensure any stale hover preview is hidden + # Close field card panel if open + game_ui.hide_field_card_selection() + # Ensure any stale hover preview is hidden game_ui.hide_card_detail() func _on_undo_requested() -> void: @@ -248,32 +265,49 @@ func _on_undo_available_changed(available: bool) -> void: func _on_table_card_clicked(card: CardInstance, zone_type: Enums.ZoneType, player_index: int) -> void: var input_mode = GameManager.input_mode + # Handle special input modes first (these take priority) match input_mode: GameManager.InputMode.SELECT_CP_SOURCE: - # Check if it's a backup we can dull if zone_type == Enums.ZoneType.FIELD_BACKUPS: if player_index == GameManager.game_state.turn_manager.current_player_index: GameManager.dull_backup_for_cp(card) _sync_visuals() _update_cp_display() + return GameManager.InputMode.SELECT_ATTACKER: - # Select attacker if zone_type == Enums.ZoneType.FIELD_FORWARDS: if player_index == GameManager.game_state.turn_manager.current_player_index: GameManager.declare_attack(card) _sync_visuals() + return GameManager.InputMode.SELECT_BLOCKER: - # Select blocker if zone_type == Enums.ZoneType.FIELD_FORWARDS: var opponent_index = 1 - GameManager.game_state.turn_manager.current_player_index if player_index == opponent_index: GameManager.declare_block(card) _sync_visuals() + return - # Show card detail on any click - game_ui.show_card_detail(card) + # Close hand selection panel if open + hand_display.deselect() + # Show field card selection panel with card image and actions + game_ui.show_field_card_selection(card, zone_type, player_index) + +func _connect_field_card_signals() -> void: + if game_ui: + game_ui.field_card_action_requested.connect(_on_field_card_action) + +func _on_field_card_action(card: CardInstance, zone_type: Enums.ZoneType, player_index: int, action: String) -> void: + match action: + "dull_cp": + GameManager.dull_backup_for_cp(card) + _sync_visuals() + _update_cp_display() + "attack": + GameManager.declare_attack(card) + _sync_visuals() func _input(event: InputEvent) -> void: # Keyboard shortcuts @@ -301,3 +335,4 @@ func _input(event: InputEvent) -> void: GameManager.clear_selection() table_setup.clear_all_highlights() hand_display.clear_highlights() + game_ui.hide_field_card_selection() diff --git a/scripts/ui/GameUI.gd b/scripts/ui/GameUI.gd index ea606c3..307340f 100644 --- a/scripts/ui/GameUI.gd +++ b/scripts/ui/GameUI.gd @@ -5,6 +5,7 @@ extends CanvasLayer 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 @@ -28,6 +29,19 @@ 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 @@ -204,6 +218,9 @@ func _create_ui() -> void: 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) @@ -347,3 +364,233 @@ func hide_card_detail() -> void: 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 diff --git a/scripts/visual/TableCamera.gd b/scripts/visual/TableCamera.gd index ecf49aa..0857b5d 100644 --- a/scripts/visual/TableCamera.gd +++ b/scripts/visual/TableCamera.gd @@ -9,45 +9,66 @@ extends Camera3D @export var camera_height_offset: float = -4.0 # Offset to look slightly above center # Smooth movement -@export var smooth_speed: float = 5.0 +@export var smooth_speed: float = 3.0 var target_position: Vector3 = Vector3.ZERO +var target_look_at: Vector3 = Vector3.ZERO +var is_animating: bool = false + +# Current player side (0 = positive Z, 1 = negative Z) +var current_player: int = 0 func _ready() -> void: - _setup_isometric_view() + _setup_for_player(0, false) -func _setup_isometric_view() -> void: +func _setup_for_player(player_index: int, animate: bool = true) -> void: # Set perspective projection projection = PROJECTION_PERSPECTIVE fov = 50.0 - # Calculate position - camera is positioned behind Player 1's side - var angle_rad = deg_to_rad(camera_angle) + current_player = player_index - # Height based on angle + var angle_rad = deg_to_rad(camera_angle) var height = camera_distance * sin(angle_rad) - # Distance along Z axis (positive Z is toward Player 1) var z_offset = camera_distance * cos(angle_rad) - # Position camera behind Player 1 (positive Z), looking toward center/opponent - position = Vector3(0, height, z_offset) + # Flip Z direction based on player + var side = 1 if player_index == 0 else -1 + var new_position = Vector3(0, height, z_offset * side) - # Look at center of table - look_at(Vector3(0, camera_height_offset, 0), Vector3.UP) + target_look_at = Vector3(0, camera_height_offset, 0) - target_position = position + if animate: + target_position = new_position + is_animating = true + else: + position = new_position + target_position = new_position + look_at(target_look_at, Vector3.UP) + is_animating = false func _process(delta: float) -> void: - # Smooth camera movement if needed - if position.distance_to(target_position) > 0.01: + if is_animating: position = position.lerp(target_position, smooth_speed * delta) - look_at(Vector3(0, camera_height_offset, 0), Vector3.UP) + look_at(target_look_at, Vector3.UP) + + if position.distance_to(target_position) < 0.05: + position = target_position + look_at(target_look_at, Vector3.UP) + is_animating = false + +## Switch camera to view from a player's perspective +func switch_to_player(player_index: int) -> void: + if player_index != current_player: + _setup_for_player(player_index, true) ## Set camera to look at a specific point func focus_on(point: Vector3) -> void: var angle_rad = deg_to_rad(camera_angle) + var side = 1 if current_player == 0 else -1 target_position = point + Vector3(0, camera_distance * sin(angle_rad), - camera_distance * cos(angle_rad)) + camera_distance * cos(angle_rad) * side) + is_animating = true ## Reset to default position func reset_position() -> void: - _setup_isometric_view() + _setup_for_player(current_player, false) diff --git a/scripts/visual/TableSetup.gd b/scripts/visual/TableSetup.gd index 7e17d47..a917ab5 100644 --- a/scripts/visual/TableSetup.gd +++ b/scripts/visual/TableSetup.gd @@ -25,16 +25,17 @@ const MAT_MARGIN: float = 0.3 # Gap between mats and from table center # Player 1 sits at +Z looking toward -Z # Player's left = +X, Player's right = -X const ZONE_POSITIONS = { - "damage": Vector3(7.0, 0.1, 1.5), # Front-left (player's left) - "deck": Vector3(-7.0, 0.1, 4.0), # Back-right (player's right, behind backups) - "break": Vector3(-7.0, 0.1, 1.5), # Front-right (player's right, front row) - "forwards": Vector3(0.0, 0.1, 2.0), # Center, front row - "backups": Vector3(0.0, 0.1, 5.0), # Center, back row + "damage": Vector3(-7.3, 0.1, 2.1), # Left column, top (forwards row) + "deck": Vector3(7.3, 0.1, 2.0), # Right column, top (forwards row) + "break": Vector3(7.3, 0.1, 5.4), # Right column, centered in break zone box + "forwards": Vector3(0.0, 0.1, 1.9), # Center, front row (near table center) + "backups": Vector3(0.0, 0.1, 4.5), # Center, back row (pulled forward to stay visible above hand) "hand": Vector3(0.0, 0.5, 7.5) # Not used on table (2D overlay) } # Background texture path const BACKGROUND_TEXTURE_PATH: String = "res://assets/table/background_1.png" +const CARD_BACK_TEXTURE_PATH: String = "res://assets/cards/card_back.png" # Components var table_mesh: MeshInstance3D @@ -195,27 +196,47 @@ func _create_zones() -> void: player_zones[player_idx]["break"] = break_zone func _create_deck_indicators() -> void: - # Create simple card-back boxes for deck representation + var card_back_texture = load(CARD_BACK_TEXTURE_PATH) + for player_idx in range(2): var flip = 1 if player_idx == 0 else -1 var pos = ZONE_POSITIONS["deck"] * Vector3(flip, 1, flip) - # Card back mesh (simple colored box) + # Deck stack (thin box for thickness) var deck_mesh = MeshInstance3D.new() add_child(deck_mesh) var box = BoxMesh.new() - box.size = Vector3(1.26, 0.3, 1.76) # Card size with thickness for deck + box.size = Vector3(1.6, 0.3, 2.2) deck_mesh.mesh = box - var mat = StandardMaterial3D.new() - mat.albedo_color = Color(0.15, 0.1, 0.3) # Dark blue for card back - deck_mesh.material_override = mat + var side_mat = StandardMaterial3D.new() + side_mat.albedo_color = Color(0.08, 0.06, 0.12) # Dark edges + deck_mesh.material_override = side_mat - deck_mesh.position = pos + Vector3(0, 0.15, 0) # Raise above table + deck_mesh.position = pos + Vector3(0, 0.15, 0) deck_indicators[player_idx] = deck_mesh + # Card back face on top of the deck + var top_card = MeshInstance3D.new() + add_child(top_card) + + var plane = PlaneMesh.new() + plane.size = Vector2(1.6, 2.2) + top_card.mesh = plane + + var top_mat = StandardMaterial3D.new() + if card_back_texture: + top_mat.albedo_texture = card_back_texture + else: + top_mat.albedo_color = Color(0.15, 0.1, 0.3) + top_card.material_override = top_mat + + top_card.position = pos + Vector3(0, 0.31, 0) + if player_idx == 1: + top_card.rotation.y = deg_to_rad(180) + # Card count label var label = Label3D.new() add_child(label) @@ -224,13 +245,13 @@ func _create_deck_indicators() -> void: label.position = pos + Vector3(0, 0.35, 0) label.rotation.x = deg_to_rad(-90) # Face up if player_idx == 1: - label.rotation.z = deg_to_rad(180) # Flip for opponent + label.rotation.z = deg_to_rad(180) deck_count_labels[player_idx] = label func _create_zone(zone_type: Enums.ZoneType, player_idx: int, pos: Vector3, rot: float) -> ZoneVisual: var zone = ZoneVisual.new() - add_child(zone) + # Set properties BEFORE add_child so _ready() uses the correct values zone.zone_type = zone_type zone.player_index = player_idx zone.zone_position = pos @@ -243,6 +264,7 @@ func _create_zone(zone_type: Enums.ZoneType, player_idx: int, pos: Vector3, rot: Enums.ZoneType.DAMAGE, Enums.ZoneType.BREAK: zone.stack_offset = 0.02 + add_child(zone) zone.card_clicked.connect(_on_zone_card_clicked.bind(zone_type, player_idx)) return zone @@ -333,3 +355,8 @@ func clear_all_highlights() -> void: var zone = player_zones[player_idx][zone_name] if zone: zone.clear_highlights() + +## Switch camera to a player's perspective +func switch_camera_to_player(player_index: int) -> void: + if camera: + camera.switch_to_player(player_index)