extends Control const CARD_NODE_SCENE := preload("res://src/ui/components/card_node.tscn") const FLIGHT_DURATION := 0.30 const COMMENTARY_FADE_IN := 0.20 const COMMENTARY_HOLD := 0.60 const COMMENTARY_FADE_OUT := 0.30 var controller: TrainingController var table_cards: Array[CardNode] = [] @onready var hand_area := $ScrollContainer/HandArea as 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 @onready var guide_label: Label = $GuideLabel @onready var table_label: Label = $TableLabel @onready var commentary_label: Label = $CommentaryLabel @onready var table_card_root: Control = $TableCardRoot func _ready() -> void: if play_button: play_button.pressed.connect(_on_play_pressed) if pass_button: pass_button.pressed.connect(_on_pass_pressed) if hint_button: hint_button.pressed.connect(_on_hint_pressed) start_training() func start_training() -> void: controller = TrainingController.new() add_child(controller) hand_area.training_controller = controller 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) controller.cards_played.connect(_on_cards_played) controller.player_passed.connect(_on_player_passed) _update_guide_text() _refresh_ui() func _update_guide_text() -> void: if guide_label: guide_label.text = "💡 提示:点击手牌选中,然后点击「出牌」或「过牌」。首次出牌建议点击「提示」按钮。" func _on_turn_ready(player_idx: int, is_human: bool) -> void: if is_human: hand_area.enable_input() if status_label: status_label.text = "你的回合 - 请出牌或点击过牌" else: hand_area.disable_input() if status_label and controller and controller.game_state: status_label.text = "%s 思考中..." % controller.game_state.player_names[player_idx] func _on_play_pressed() -> void: if not hand_area: return var selected := hand_area.selected_cards if selected.is_empty(): if status_label: status_label.text = "请先选择要出的牌" return var result := controller.handle_human_play(selected) if not result.ok: var error_msg := _get_error_message(result.error_code) if status_label: status_label.text = "❌ %s" % error_msg return hand_area.clear_selection() # 不立即刷新,等 cards_played 信号触发刷新 func _on_pass_pressed() -> void: var result := controller.handle_human_pass() if not result.ok: var error_msg := _get_error_message(result.error_code) if status_label: status_label.text = "❌ %s" % error_msg return if status_label: status_label.text = "✓ 已过牌" func _on_cards_played(player_idx: int, play: HandEvaluator.EvaluatedPlay) -> void: hand_area.disable_input() var sorted_cards: Array[Card] = [] for c in play.cards: sorted_cards.append(c) sorted_cards.sort_custom(func(a: Card, b: Card): return a.compare_to(b) < 0) var card_w := 60.0 var total_w := card_w * sorted_cards.size() var start_x := (1280.0 - total_w) / 2.0 var table_y := 80.0 var ghosts: Array[CardNode] = [] var targets: Array[Vector2] = [] for i in range(sorted_cards.size()): var src_node := _find_hand_node(sorted_cards[i]) var src_pos := src_node.global_position if src_node else Vector2(start_x + i * card_w, 600.0) var ghost := CARD_NODE_SCENE.instantiate() as CardNode get_tree().root.add_child(ghost) ghost.setup(sorted_cards[i]) ghost.global_position = src_pos ghost.mouse_filter = Control.MOUSE_FILTER_IGNORE ghosts.append(ghost) targets.append(Vector2(start_x + i * card_w, table_y)) for i in range(ghosts.size()): var g := ghosts[i] var t := create_tween().set_parallel(true) t.tween_property(g, "global_position", targets[i], FLIGHT_DURATION) \ .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) t.tween_property(g, "scale", Vector2(0.7, 0.7), FLIGHT_DURATION) \ .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) await get_tree().create_timer(FLIGHT_DURATION).timeout for g in ghosts: g.queue_free() _show_table_cards(sorted_cards) _show_commentary(player_idx, play) await get_tree().create_timer(COMMENTARY_HOLD).timeout var fade := create_tween() fade.tween_property(commentary_label, "modulate:a", 0.0, COMMENTARY_FADE_OUT) await get_tree().create_timer(COMMENTARY_FADE_OUT).timeout _clear_table_cards() _refresh_ui() func _on_player_passed(player_idx: int) -> void: var player_name := controller.game_state.player_names[player_idx] if status_label: status_label.text = "%s 过牌" % player_name # 清除桌面牌 _clear_table_cards() await get_tree().create_timer(0.5).timeout _refresh_ui() func _show_table_cards(cards: Array[Card]) -> void: _clear_table_cards() # 按 rank 排序 var sorted_cards: Array[Card] = cards.duplicate(false) sorted_cards.sort_custom(func(a: Card, b: Card): return a.compare_to(b) < 0) var total_width := sorted_cards.size() * 60 var start_x := (1280 - total_width) / 2 for i in range(sorted_cards.size()): var card: Card = sorted_cards[i] var node := CARD_NODE_SCENE.instantiate() as CardNode if node == null: continue table_card_root.add_child(node) node.setup(card) node.position = Vector2(start_x + i * 60, 80) node.custom_minimum_size = Vector2(55, 80) node.size = Vector2(55, 80) node.mouse_filter = Control.MOUSE_FILTER_IGNORE table_cards.append(node) func _clear_table_cards() -> void: for cn in table_cards: cn.queue_free() table_cards.clear() func _show_commentary(player_idx: int, play: HandEvaluator.EvaluatedPlay) -> void: if not commentary_label: return var pname := controller.game_state.player_names[player_idx] var type_name := _get_type_name(play.type) commentary_label.text = "%s 出了 %s(%d张,主阶=%d)" \ % [pname, type_name, play.cards.size(), play.primary_rank] commentary_label.modulate.a = 0.0 var t := create_tween() t.tween_property(commentary_label, "modulate:a", 1.0, COMMENTARY_FADE_IN) func _find_hand_node(card: Card) -> CardNode: for cn in hand_area.card_nodes: if cn.card_data != null and cn.card_data.card_id == card.card_id: return cn return null func _get_error_message(error_code: int) -> String: match error_code: 1: return "无效的牌型组合" 2: return "不是你的回合" 3: return "不能过牌(你是领出者)" 4: return "牌不在手中" _: return "出牌失败" func _on_hint_pressed() -> void: var hint := controller.get_hint() if hint == null or hint.type == -1: if status_label: status_label.text = "建议:过牌(当前轮到你领出,但无合适牌型)" return if not hand_area: 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) var type_name := _get_type_name(hint.type) if status_label: status_label.text = "💡 建议:出 %s(%d张,主阶=%d)" % [type_name, hint.cards.size(), hint.primary_rank] func _get_type_name(type_idx: int) -> String: match type_idx: 0: return "单张" 1: return "对子" 2: return "三张" 3: return "三带二" 4: return "顺子" 5: return "连对" 6: return "钢板" 7: return "同花顺" 8: return "炸弹" 9: return "火箭" _: return "未知" func _on_game_ended(winner_team: int, _reason: String) -> void: if status_label: status_label.text = "游戏结束! 队伍 %d 获胜" % winner_team if hand_area: hand_area.disable_input() func _refresh_ui() -> void: if controller and controller.game_state and hand_area: var hand: Array[Card] = controller.game_state.get_hand(0) hand_area.update_hand(hand)