From 3272e3dc0aa121f055d8b51eb2782b3ffc3fc1c9 Mon Sep 17 00:00:00 2001 From: xiaji Date: Fri, 29 May 2026 09:14:42 +0800 Subject: [PATCH] feat: add basic UI scenes (card, hand, training room, main menu) --- src/ui/components/card_node.gd | 37 +++++++++++++++++ src/ui/components/card_node.tscn | 16 ++++++++ src/ui/components/hand_area.gd | 61 ++++++++++++++++++++++++++++ src/ui/components/hand_area.tscn | 6 +++ src/ui/scenes/main_menu.gd | 13 ++++++ src/ui/scenes/main_menu.tscn | 17 ++++++++ src/ui/scenes/training_room.gd | 70 ++++++++++++++++++++++++++++++++ src/ui/scenes/training_room.tscn | 28 +++++++++++++ 8 files changed, 248 insertions(+) create mode 100644 src/ui/components/card_node.gd create mode 100644 src/ui/components/card_node.tscn create mode 100644 src/ui/components/hand_area.gd create mode 100644 src/ui/components/hand_area.tscn create mode 100644 src/ui/scenes/main_menu.gd create mode 100644 src/ui/scenes/main_menu.tscn create mode 100644 src/ui/scenes/training_room.gd create mode 100644 src/ui/scenes/training_room.tscn diff --git a/src/ui/components/card_node.gd b/src/ui/components/card_node.gd new file mode 100644 index 0000000..78ade9d --- /dev/null +++ b/src/ui/components/card_node.gd @@ -0,0 +1,37 @@ +extends Control + +signal card_clicked(card_node) +signal card_double_clicked(card_node) + +var card_data: Card = null +var is_selected: bool = false + +@onready var texture_rect: TextureRect = $TextureRect +@onready var label: Label = $Label + +func setup(card: Card) -> void: + card_data = card + update_display() + +func update_display() -> void: + if card_data == null: + return + var suits := ["S", "H", "C", "D", "SJ", "BJ"] + var ranks := ["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "SJ", "BJ"] + var suit := card_data.suit() + var rank := card_data.rank() + if rank < ranks.size() and suit < suits.size(): + label.text = "%s %s" % [suits[suit], ranks[rank]] + modulate = Color.WHITE if not is_selected else Color(1.2, 1.2, 0.8) + +func set_selected(sel: bool) -> void: + is_selected = sel + update_display() + +func _on_gui_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: + if event.double_click: + card_double_clicked.emit(self) + else: + card_clicked.emit(self) diff --git a/src/ui/components/card_node.tscn b/src/ui/components/card_node.tscn new file mode 100644 index 0000000..701cf26 --- /dev/null +++ b/src/ui/components/card_node.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=2 format=3 uid="uid://card_node"] +[ext_resource type="Script" path="res://src/ui/components/card_node.gd" id="1_script"] +[node name="CardNode" type="Control"] +custom_minimum_size = Vector2(80, 120) +size = Vector2(80, 120) +script = ExtResource("1_script") +[node name="TextureRect" type="TextureRect" parent="."] +layout_mode = 0 +offset_right = 80.0 +offset_bottom = 120.0 +[node name="Label" type="Label" parent="."] +layout_mode = 0 +offset_right = 80.0 +offset_bottom = 120.0 +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/src/ui/components/hand_area.gd b/src/ui/components/hand_area.gd new file mode 100644 index 0000000..0ec9115 --- /dev/null +++ b/src/ui/components/hand_area.gd @@ -0,0 +1,61 @@ +extends HBoxContainer + +signal cards_selected(selected: Array) +signal hint_requested() +signal play_requested() +signal pass_requested() + +var card_nodes: Array = [] +var selected_cards: Array = [] +const CARD_SCENE := preload("res://src/ui/components/card_node.tscn") + +var training_controller: TrainingController = null + +func update_hand(hand: Array) -> void: + for cn in card_nodes: + cn.queue_free() + card_nodes.clear() + selected_cards.clear() + for c in hand: + var node: Control = CARD_SCENE.instantiate() + node.setup(c) + node.card_clicked.connect(_on_card_clicked) + node.card_double_clicked.connect(_on_card_double_clicked) + add_child(node) + card_nodes.append(node) + +func _on_card_clicked(card_node: Node) -> void: + card_node.set_selected(not card_node.is_selected) + if card_node.is_selected: + selected_cards.append(card_node.card_data) + else: + selected_cards.erase(card_node.card_data) + cards_selected.emit(selected_cards) + +func _on_card_double_clicked(card_node: Node) -> void: + if not card_node.is_selected: + card_node.set_selected(true) + selected_cards.append(card_node.card_data) + play_requested.emit() + +func _on_hint_pressed() -> void: + hint_requested.emit() + +func _on_play_pressed() -> void: + play_requested.emit() + +func _on_pass_pressed() -> void: + pass_requested.emit() + +func disable_input() -> void: + for cn in card_nodes: + cn.mouse_filter = Control.MOUSE_FILTER_IGNORE + +func enable_input() -> void: + for cn in card_nodes: + cn.mouse_filter = Control.MOUSE_FILTER_STOP + +func clear_selection() -> void: + for cn in card_nodes: + cn.set_selected(false) + selected_cards.clear() diff --git a/src/ui/components/hand_area.tscn b/src/ui/components/hand_area.tscn new file mode 100644 index 0000000..6b66f2f --- /dev/null +++ b/src/ui/components/hand_area.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://hand_area"] +[ext_resource type="Script" path="res://src/ui/components/hand_area.gd" id="1_script"] +[node name="HandArea" type="HBoxContainer"] +offset_right = 800.0 +offset_bottom = 150.0 +script = ExtResource("1_script") diff --git a/src/ui/scenes/main_menu.gd b/src/ui/scenes/main_menu.gd new file mode 100644 index 0000000..b1696a8 --- /dev/null +++ b/src/ui/scenes/main_menu.gd @@ -0,0 +1,13 @@ +extends Control + +func _ready() -> void: + var start_btn := $VBoxContainer/StartButton as Button + var quit_btn := $VBoxContainer/QuitButton as Button + start_btn.pressed.connect(_on_start_pressed) + quit_btn.pressed.connect(_on_quit_pressed) + +func _on_start_pressed() -> void: + get_tree().change_scene_to_file("res://src/ui/scenes/training_room.tscn") + +func _on_quit_pressed() -> void: + get_tree().quit() diff --git a/src/ui/scenes/main_menu.tscn b/src/ui/scenes/main_menu.tscn new file mode 100644 index 0000000..ea79f8f --- /dev/null +++ b/src/ui/scenes/main_menu.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=2 format=3 uid="uid://main_menu"] +[ext_resource type="Script" path="res://src/ui/scenes/main_menu.gd" id="1_script"] +[node name="MainMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +script = ExtResource("1_script") +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 8 +offset_left = 300.0 +offset_top = 200.0 +offset_right = 500.0 +offset_bottom = 400.0 +[node name="StartButton" type="Button" parent="VBoxContainer"] +text = "开始训练" +[node name="QuitButton" type="Button" parent="VBoxContainer"] +text = "退出" diff --git a/src/ui/scenes/training_room.gd b/src/ui/scenes/training_room.gd new file mode 100644 index 0000000..17538fd --- /dev/null +++ b/src/ui/scenes/training_room.gd @@ -0,0 +1,70 @@ +extends Control + +var controller: TrainingController + +@onready var hand_area: Node = $HandArea +@onready var play_button: Button = $Buttons/PlayButton +@onready var pass_button: Button = $Buttons/PassButton +@onready var hint_button: Button = $Buttons/HintButton +@onready var status_label: Label = $StatusLabel + +func _ready() -> void: + hand_area.training_controller = controller + play_button.pressed.connect(_on_play_pressed) + pass_button.pressed.connect(_on_pass_pressed) + hint_button.pressed.connect(_on_hint_pressed) + +func start_training() -> void: + controller = TrainingController.new() + controller.start_game(Config.rule_config, 0) + controller.turn_ready.connect(_on_turn_ready) + controller.state_changed.connect(_refresh_ui) + controller.game_ended.connect(_on_game_ended) + _refresh_ui() + +func _on_turn_ready(player_idx: int, is_human: bool) -> void: + if is_human: + hand_area.enable_input() + status_label.text = "你的回合" + else: + hand_area.disable_input() + status_label.text = "%s 思考中..." % controller.game_state.player_names[player_idx] + +func _on_play_pressed() -> void: + var selected := hand_area.selected_cards + if selected.is_empty(): + return + var result := controller.handle_human_play(selected) + if not result.ok: + status_label.text = "无效出牌" + return + hand_area.clear_selection() + _refresh_ui() + +func _on_pass_pressed() -> void: + var result := controller.handle_human_pass() + if not result.ok: + status_label.text = "不能过牌" + return + _refresh_ui() + +func _on_hint_pressed() -> void: + var hint := controller.get_hint() + if hint == null or hint.type == -1: + status_label.text = "建议:过牌" + return + hand_area.clear_selection() + for card in hint.cards: + for cn in hand_area.card_nodes: + if cn.card_data != null and cn.card_data.card_id == card.card_id: + cn.set_selected(true) + hand_area.selected_cards.append(card) + status_label.text = "建议牌型: %s (rank=%d)" % [hint.type, hint.primary_rank] + +func _on_game_ended(winner_team: int, reason: String) -> void: + status_label.text = "游戏结束! 队伍 %d 获胜" % winner_team + hand_area.disable_input() + +func _refresh_ui() -> void: + if controller and controller.game_state: + hand_area.update_hand(controller.game_state.get_hand(0)) diff --git a/src/ui/scenes/training_room.tscn b/src/ui/scenes/training_room.tscn new file mode 100644 index 0000000..2de8d6c --- /dev/null +++ b/src/ui/scenes/training_room.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=3 uid="uid://training_room"] +[ext_resource type="Script" path="res://src/ui/scenes/training_room.gd" id="1_script"] +[node name="TrainingRoom" type="Control"] +layout_mode = 3 +anchors_preset = 15 +script = ExtResource("1_script") +[node name="StatusLabel" type="Label" parent="."] +layout_mode = 0 +offset_right = 400.0 +offset_bottom = 50.0 +text = "掼蛋训练模式" +horizontal_alignment = 1 +[node name="HandArea" type="HBoxContainer" parent="."] +layout_mode = 0 +offset_top = 500.0 +offset_right = 800.0 +offset_bottom = 650.0 +[node name="Buttons" type="HBoxContainer" parent="."] +layout_mode = 0 +offset_top = 660.0 +offset_right = 800.0 +offset_bottom = 720.0 +[node name="PlayButton" type="Button" parent="Buttons"] +text = "出牌" +[node name="PassButton" type="Button" parent="Buttons"] +text = "过牌" +[node name="HintButton" type="Button" parent="Buttons"] +text = "提示"