From 70bd359f8a22675ab9b2b5d04b770feac1e514f0 Mon Sep 17 00:00:00 2001 From: Tiger Date: Mon, 22 Jun 2026 11:40:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=B1AI=E8=BE=85=E5=8A=A9=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E6=B8=B8=E6=88=8F=E5=86=85=E5=AE=B9=E7=BC=96=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ai-summon/GameLoopManager.gd | 317 -------- ai-summon/GameLoopManager.gd.uid | 1 - ai-summon/README.md | 5 - scenes/game/card.tscn | 82 +- scenes/game/game.tscn | 136 ++-- scripts/autoload/GameLoopManager.gd | 1170 +++++++++++++++++++++++++-- scripts/game/card.gd | 416 +++++++++- scripts/game/game.gd | 318 +++++++- 9 files changed, 1992 insertions(+), 455 deletions(-) delete mode 100644 ai-summon/GameLoopManager.gd delete mode 100644 ai-summon/GameLoopManager.gd.uid delete mode 100644 ai-summon/README.md diff --git a/README.md b/README.md index bdeeff3..17ecc5b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ game/ ## 最佳实践 -本项目目前正在使用 Godot 4.6.2 进行开发。开发用语言为 GDScript。 +本项目目前正在使用 Godot 4.7 进行开发。开发用语言为 GDScript。 你的开发应当遵循 Godot 引擎提供的[最佳实践](https://docs.godotengine.org/zh-cn/4.x/tutorials/best_practices/)及[GDScript 编写风格指南](https://docs.godotengine.org/zh-cn/4.x/tutorials/scripting/gdscript/gdscript_styleguide.html)。但下面提到的除外: diff --git a/ai-summon/GameLoopManager.gd b/ai-summon/GameLoopManager.gd deleted file mode 100644 index 06d227b..0000000 --- a/ai-summon/GameLoopManager.gd +++ /dev/null @@ -1,317 +0,0 @@ -extends Node - -var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() - -var max_player_num: int -var player_num: int - -# 玩家数据字典 [peer_id: {"name": str, "hand": Array, "score": int, "is_ready": bool}] -var players_data: Dictionary = {} -var current_turn: int = 1 # 当前回合数 -var current_player_id: int = 1 # 当前行动玩家的 peer_id -var game_started: bool = false -var game_phase: String = "waiting" # waiting, playing, ended -var deck: Array = [] # 游戏牌组 - -func create_server(playern: int) -> void: - max_player_num = playern - player_num = 1 - - if peer.create_server(GameManager.port, playern) != OK: - print("Failed to create server on port: ", GameManager.port) - return - - setup_multiplayer() - - # 服务器自己也是玩家 - players_data[1] = { - "name": "Host", - "hand": [], - "score": 0, - "is_ready": false - } - print("Server created on port: ", GameManager.port) - -func create_client(ip: String) -> void: - if peer.create_client(ip, GameManager.port) != OK: - print("Failed to connect to server: ", ip) - return - - setup_multiplayer() - print("Connecting to server: ", ip) - -func setup_multiplayer() -> void: - multiplayer.multiplayer_peer = peer - multiplayer.peer_connected.connect(_on_peer_connected) - multiplayer.peer_disconnected.connect(_on_peer_disconnected) - multiplayer.connected_to_server.connect(_on_connected_to_server) - multiplayer.connection_failed.connect(_on_connection_failed) - -func _on_connected_to_server() -> void: - print("Connected to server successfully!") - -func _on_connection_failed() -> void: - print("Failed to connect to server") - -func _on_peer_connected(id: int) -> void: - print("Peer connected: ", id) - add_player(id) - - # 如果游戏未开始且达到最大玩家数,询问是否开始游戏 - if !game_started and players_data.size() >= max_player_num: - check_all_players_ready() - -func _on_peer_disconnected(id: int): - print("Peer disconnected: ", id) - remove_player(id) - - # 如果当前玩家断开,切换到下一个玩家 - if id == current_player_id and game_started: - next_turn() - -func add_player(id: int) -> void: - # 只有服务器才能添加玩家 - if multiplayer.is_server(): - players_data[id] = { - "name": "Player_" + str(id), - "hand": [], - "score": 0, - "is_ready": false - } - player_num = players_data.size() - - # 广播新玩家加入 - broadcast_player_list() - print("Player added: ", id, " Total players: ", player_num) - -func remove_player(id: int) -> void: - if multiplayer.is_server() and players_data.has(id): - players_data.erase(id) - player_num = players_data.size() - broadcast_player_list() - print("Player removed: ", id) - -# RPC: 设置玩家准备状态 -@rpc("any_peer", "call_remote", "reliable") -func set_player_ready(is_ready: bool) -> void: - var peer_id = multiplayer.get_remote_sender_id() - if players_data.has(peer_id): - players_data[peer_id]["is_ready"] = is_ready - broadcast_player_list() - - if is_ready: - check_all_players_ready() - -# RPC: 设置玩家名称 -@rpc("any_peer", "call_remote", "reliable") -func set_player_name(name: String) -> void: - var peer_id = multiplayer.get_remote_sender_id() - if players_data.has(peer_id): - players_data[peer_id]["name"] = name - broadcast_player_list() - -# RPC: 开始游戏(仅主机调用) -@rpc("call_remote", "reliable") -func start_game() -> void: - if multiplayer.is_server(): - if players_data.size() < 2: - print("Need at least 2 players to start game") - return - - game_started = true - game_phase = "playing" - current_turn = 1 - initialize_deck() - deal_initial_cards() - - # 随机选择第一个玩家 - var player_ids = players_data.keys() - current_player_id = player_ids[randi() % player_ids.size()] - - print("Game started! Current player: ", current_player_id) - broadcast_game_state() - -# 初始化牌组 -func initialize_deck() -> void: - deck = [] - # 这里可以根据 GameManager 中的卡牌列表初始化牌组 - if GameManager.card_list: - for card_id in GameManager.card_list: - # 每张卡牌加入多次(根据游戏需求调整) - for i in range(3): # 每张卡牌3份 - deck.append(card_id) - - # 洗牌 - deck.shuffle() - print("Deck initialized with ", deck.size(), " cards") - -# 发放初始手牌 -func deal_initial_cards() -> void: - var cards_per_player = 5 # 每个玩家初始手牌数 - - for peer_id in players_data: - for i in range(cards_per_player): - if deck.size() > 0: - var card_id = deck.pop_front() - players_data[peer_id]["hand"].append(card_id) - - broadcast_hands() - -# RPC: 抽牌 -@rpc("any_peer", "call_remote", "reliable") -func draw_card() -> void: - var peer_id = multiplayer.get_remote_sender_id() - - # 检查是否是该玩家的回合 - if peer_id != current_player_id: - return - - # 检查牌组是否还有牌 - if deck.size() == 0: - print("Deck is empty!") - return - - var card_id = deck.pop_front() - players_data[peer_id]["hand"].append(card_id) - - print("Player ", peer_id, " drew card: ", card_id) - broadcast_hands() - -# RPC: 打出卡牌 -@rpc("any_peer", "call_remote", "reliable") -func play_card(card_index: int, target_player_id: int = -1) -> void: - var peer_id = multiplayer.get_remote_sender_id() - - # 检查是否是该玩家的回合 - if peer_id != current_player_id: - return - - # 检查卡牌索引是否有效 - if card_index < 0 or card_index >= players_data[peer_id]["hand"].size(): - return - - var card_id = players_data[peer_id]["hand"][card_index] - - # 移除手牌中的卡牌 - players_data[peer_id]["hand"].remove_at(card_index) - - # 执行卡牌效果 - execute_card_effect(card_id, peer_id, target_player_id) - - print("Player ", peer_id, " played card: ", card_id) - broadcast_hands() - broadcast_game_state() - -# 执行卡牌效果 -func execute_card_effect(card_id: int, player_id: int, target_id: int) -> void: - # 这里根据卡牌ID执行具体效果 - # 示例:简单加分效果 - if players_data.has(player_id): - players_data[player_id]["score"] += 10 - - # 可以添加更多复杂的卡牌效果逻辑 - print("Card ", card_id, " effect executed by player ", player_id) - -# RPC: 结束回合 -@rpc("any_peer", "call_remote", "reliable") -func end_turn() -> void: - var peer_id = multiplayer.get_remote_sender_id() - - if peer_id != current_player_id: - return - - next_turn() - -# 切换到下一个玩家 -func next_turn() -> void: - var player_ids = players_data.keys() - var current_index = player_ids.find(current_player_id) - - if current_index != -1: - current_index = (current_index + 1) % player_ids.size() - current_player_id = player_ids[current_index] - current_turn += 1 - - print("Turn ", current_turn, ". Current player: ", current_player_id) - broadcast_game_state() - -# 检查所有玩家是否都准备好了 -func check_all_players_ready() -> void: - var all_ready = true - for player_id in players_data: - if !players_data[player_id]["is_ready"]: - all_ready = false - break - - if all_ready and players_data.size() >= 2: - print("All players ready! Starting game...") - start_game.rpc() - -# 广播玩家列表 -func broadcast_player_list() -> void: - update_players_data.rpc(players_data) - -# RPC: 更新玩家数据 -@rpc("call_remote", "reliable") -func update_players_data(data: Dictionary) -> void: - players_data = data - player_num = players_data.size() - emit_signal("players_updated", players_data) - -# 广播游戏状态 -func broadcast_game_state() -> void: - update_game_state.rpc(game_started, game_phase, current_turn, current_player_id) - -# RPC: 更新游戏状态 -@rpc("call_remote", "reliable") -func update_game_state(started: bool, phase: String, turn: int, current_id: int) -> void: - game_started = started - game_phase = phase - current_turn = turn - current_player_id = current_id - emit_signal("game_state_updated", game_started, game_phase, current_turn, current_player_id) - -# 广播手牌信息(只发给对应玩家) -func broadcast_hands() -> void: - for peer_id in players_data: - send_hand.rpc_id(peer_id, players_data[peer_id]["hand"]) - - # 发送其他玩家的手牌数量 - var hand_sizes = {} - for peer_id in players_data: - hand_sizes[peer_id] = players_data[peer_id]["hand"].size() - broadcast_hand_sizes.rpc(hand_sizes) - -# RPC: 发送手牌 -@rpc("call_remote", "reliable") -func send_hand(hand: Array) -> void: - var peer_id = multiplayer.get_unique_id() - if players_data.has(peer_id): - players_data[peer_id]["hand"] = hand - emit_signal("hand_updated", hand) - -# RPC: 广播手牌数量 -@rpc("call_remote", "reliable") -func broadcast_hand_sizes(sizes: Dictionary) -> void: - emit_signal("hand_sizes_updated", sizes) - -# RPC: 结束游戏 -@rpc("call_remote", "reliable") -func end_game() -> void: - game_started = false - game_phase = "ended" - broadcast_game_state() - - # 计算并显示最终得分 - var scores = {} - for player_id in players_data: - scores[player_id] = players_data[player_id]["score"] - - emit_signal("game_ended", scores) - -# 信号定义(需要在编辑器中连接或在其他脚本中连接) -signal players_updated(players: Dictionary) -signal game_state_updated(started: bool, phase: String, turn: int, current_player: int) -signal hand_updated(hand: Array) -signal hand_sizes_updated(sizes: Dictionary) -signal game_ended(scores: Dictionary) \ No newline at end of file diff --git a/ai-summon/GameLoopManager.gd.uid b/ai-summon/GameLoopManager.gd.uid deleted file mode 100644 index c92d0e7..0000000 --- a/ai-summon/GameLoopManager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dkwc5qney4yee diff --git a/ai-summon/README.md b/ai-summon/README.md deleted file mode 100644 index 5a71200..0000000 --- a/ai-summon/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# AI 生成代码文件 - -这个目录下的代码都是 AI 生成的,仅作参考。 - -不要将本目录下的代码直接复制到被使用的代码处。 \ No newline at end of file diff --git a/scenes/game/card.tscn b/scenes/game/card.tscn index a6a895a..418218e 100644 --- a/scenes/game/card.tscn +++ b/scenes/game/card.tscn @@ -1,8 +1,86 @@ [gd_scene format=3 uid="uid://croqe86tpmsco"] [ext_resource type="Script" uid="uid://di3cxx8th55lg" path="res://scripts/game/card.gd" id="1_3aeal"] +[ext_resource type="FontFile" uid="uid://c2bipf4tpdjj8" path="res://assets/fonts/AlibabaPuHuiTi-3-65-Medium.ttf" id="2_font"] -[node name="Card" type="Node2D" unique_id=1826213731] +[node name="Card" type="Node2D"] script = ExtResource("1_3aeal") -[node name="Sprite" type="Sprite2D" parent="." unique_id=1627492935] +[node name="Background" type="ColorRect" parent="."] +offset_left = 0.0 +offset_top = 0.0 +offset_right = 120.0 +offset_bottom = 180.0 +color = Color(0.75, 0.75, 0.82, 1) + +[node name="Sprite" type="Sprite2D" parent="."] +position = Vector2(60, 90) + +[node name="NameLabel" type="Label" parent="."] +offset_left = 5.0 +offset_top = 5.0 +offset_right = 115.0 +offset_bottom = 35.0 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 18 +text = "" +horizontal_alignment = 0 + +[node name="DescriptionLabel" type="Label" parent="."] +offset_left = 5.0 +offset_top = 140.0 +offset_right = 115.0 +offset_bottom = 175.0 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 12 +text = "" +horizontal_alignment = 0 + +[node name="ElementLabel" type="Label" parent="."] +offset_left = 5.0 +offset_top = 35.0 +offset_right = 115.0 +offset_bottom = 70.0 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 24 +text = "" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="FormulaLabel" type="Label" parent="."] +offset_left = 5.0 +offset_top = 35.0 +offset_right = 115.0 +offset_bottom = 70.0 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 20 +text = "" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="EffectLabel" type="Label" parent="."] +offset_left = 5.0 +offset_top = 35.0 +offset_right = 115.0 +offset_bottom = 70.0 +theme_override_fonts/font = ExtResource("2_font") +theme_override_font_sizes/font_size = 16 +text = "" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="SelectionHighlight" type="ColorRect" parent="."] +visible = false +offset_left = -3.0 +offset_top = -3.0 +offset_right = 123.0 +offset_bottom = 183.0 +color = Color(1, 1, 0.3, 0.5) + +[node name="HighlightOverlay" type="ColorRect" parent="."] +visible = false +offset_left = 0.0 +offset_top = 0.0 +offset_right = 120.0 +offset_bottom = 180.0 +color = Color(0.3, 1, 0.3, 0.3) \ No newline at end of file diff --git a/scenes/game/game.tscn b/scenes/game/game.tscn index 8a1fd6d..b440d42 100644 --- a/scenes/game/game.tscn +++ b/scenes/game/game.tscn @@ -3,17 +3,17 @@ [ext_resource type="FontFile" uid="uid://c2bipf4tpdjj8" path="res://assets/fonts/AlibabaPuHuiTi-3-65-Medium.ttf" id="1_uwrxv"] [ext_resource type="Script" uid="uid://78lmrp321x85" path="res://scripts/game/game.gd" id="1_yqjtg"] -[node name="Game" type="Node2D" unique_id=818469863] +[node name="Game" type="Node2D" unique_id=736592237] script = ExtResource("1_yqjtg") -[node name="Background" type="ColorRect" parent="." unique_id=802635055] +[node name="Background" type="ColorRect" parent="." unique_id=1805986496] offset_left = -29.0 offset_top = -18.0 offset_right = 2599.0 offset_bottom = 1485.0 color = Color(0.7058824, 0.6862745, 0, 1) -[node name="IsServerLabel" type="Label" parent="." unique_id=592205212] +[node name="IsServerLabel" type="Label" parent="." unique_id=1073183772] offset_left = 58.0 offset_top = 15.0 offset_right = 255.0 @@ -22,7 +22,7 @@ theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 20 text = "GAMEUI_URNULL" -[node name="IPLabel" type="Label" parent="." unique_id=2017838611] +[node name="IPLabel" type="Label" parent="." unique_id=1989497270] offset_left = 54.0 offset_top = 53.0 offset_right = 156.0 @@ -31,60 +31,106 @@ theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 24 text = "127.0.0.2" -[node name="Player1" type="Node2D" parent="." unique_id=1336934388] +[node name="PhaseLabel" type="Label" parent="." unique_id=1288595795] +offset_left = 525.0 +offset_top = 15.0 +offset_right = 825.0 +offset_bottom = 63.0 +theme_override_fonts/font = ExtResource("1_uwrxv") +theme_override_font_sizes/font_size = 28 -[node name="Username" type="Label" parent="Player1" unique_id=264475054] +[node name="RoundLabel" type="Label" parent="." unique_id=1408966789] +offset_left = 825.0 +offset_top = 15.0 +offset_right = 1025.0 +offset_bottom = 63.0 +theme_override_fonts/font = ExtResource("1_uwrxv") +theme_override_font_sizes/font_size = 28 + +[node name="EndPhaseButton" type="Button" parent="." unique_id=1666060569] +offset_left = 2074.0 +offset_top = 1332.0 +offset_right = 2374.0 +offset_bottom = 1382.0 +theme_override_fonts/font = ExtResource("1_uwrxv") +theme_override_font_sizes/font_size = 24 +text = "结束阶段" + +[node name="NotificationLabel" type="Label" parent="." unique_id=489502093] +offset_left = 525.0 +offset_top = 580.0 +offset_right = 2025.0 +offset_bottom = 640.0 +theme_override_fonts/font = ExtResource("1_uwrxv") +theme_override_font_sizes/font_size = 40 +horizontal_alignment = 1 + +[node name="Player1" type="Node2D" parent="." unique_id=2011511951] + +[node name="Username" type="Label" parent="Player1" unique_id=29274612] offset_left = 70.0 -offset_top = 1338.0 -offset_right = 552.0 -offset_bottom = 1396.0 +offset_top = 1300.0 +offset_right = 470.0 +offset_bottom = 1360.0 theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 35 text = "Player1" -[node name="CardAreaPanel" type="PanelContainer" parent="Player1" unique_id=881125453] -offset_left = 525.0 -offset_top = 1194.0 -offset_right = 1967.0 -offset_bottom = 1400.0 - -[node name="ReactionAreaPanel" type="PanelContainer" parent="Player1" unique_id=1328772147] -offset_left = 530.0 -offset_top = 700.0 -offset_right = 1972.0 -offset_bottom = 1139.0 - -[node name="Label" type="Label" parent="Player1" unique_id=739321767] -offset_left = 71.0 -offset_top = 1292.0 -offset_right = 352.0 -offset_bottom = 1353.0 +[node name="ElectronLabel" type="Label" parent="Player1" unique_id=7738246] +offset_left = 70.0 +offset_top = 1240.0 +offset_right = 370.0 +offset_bottom = 1300.0 theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 35 -text = "HP: 5" +text = "电子层: 3/3" -[node name="Player2" type="Node2D" parent="." unique_id=1559551502] +[node name="CardAreaPanel" type="PanelContainer" parent="Player1" unique_id=511752962] +offset_left = 525.0 +offset_top = 1200.0 +offset_right = 2025.0 +offset_bottom = 1400.0 -[node name="Username" type="Label" parent="Player2" unique_id=1130366682] -offset_left = 2121.0 -offset_top = 36.0 -offset_right = 2434.0 -offset_bottom = 94.0 +[node name="ReactionAreaPanel" type="PanelContainer" parent="Player1" unique_id=531235727] +offset_left = 525.0 +offset_top = 650.0 +offset_right = 2025.0 +offset_bottom = 1150.0 + +[node name="ElementAreaPanel" type="PanelContainer" parent="Player1" unique_id=699278238] +offset_left = 70.0 +offset_top = 650.0 +offset_right = 520.0 +offset_bottom = 1150.0 + +[node name="Player2" type="Node2D" parent="." unique_id=74741602] + +[node name="Username" type="Label" parent="Player2" unique_id=510041133] +offset_left = 2100.0 +offset_top = 40.0 +offset_right = 2400.0 +offset_bottom = 100.0 theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 35 text = "Player2" -[node name="ReactionAreaPanel" type="PanelContainer" parent="Player2" unique_id=470786359] -offset_left = 525.0 -offset_top = 15.0 -offset_right = 1967.0 -offset_bottom = 425.0 - -[node name="Label" type="Label" parent="Player2" unique_id=1027772856] -offset_left = 2127.0 -offset_top = 104.0 -offset_right = 2333.0 -offset_bottom = 165.0 +[node name="ElectronLabel" type="Label" parent="Player2" unique_id=340501131] +offset_left = 2100.0 +offset_top = 100.0 +offset_right = 2400.0 +offset_bottom = 160.0 theme_override_fonts/font = ExtResource("1_uwrxv") theme_override_font_sizes/font_size = 35 -text = "HP: 5" +text = "电子层: 3/3" + +[node name="ReactionAreaPanel" type="PanelContainer" parent="Player2" unique_id=1230613866] +offset_left = 525.0 +offset_top = 15.0 +offset_right = 2025.0 +offset_bottom = 515.0 + +[node name="ElementAreaPanel" type="PanelContainer" parent="Player2" unique_id=1343260677] +offset_left = 2100.0 +offset_top = 160.0 +offset_right = 2500.0 +offset_bottom = 515.0 diff --git a/scripts/autoload/GameLoopManager.gd b/scripts/autoload/GameLoopManager.gd index 56bc320..b4f7486 100644 --- a/scripts/autoload/GameLoopManager.gd +++ b/scripts/autoload/GameLoopManager.gd @@ -1,31 +1,127 @@ extends Node +## ============================================================ +## 纸片化学(物质攻防规则)- 游戏循环管理器 +## GameLoopManager.gd - 管理游戏流程、回合、结算等核心逻辑 +## ============================================================ + +# ---- 信号 ---- +signal game_phase_changed(phase: GamePhase, player_id: int) +signal player_data_updated(player_id: int) +signal player_eliminated(player_id: int, attacker_id: int) +signal player_dying_entered(player_id: int) +signal player_dying_resolved(player_id: int, survived: bool) +signal hand_updated(player_id: int, cards: Array) +signal reaction_area_updated(player_id: int, substances: Array) +signal element_area_updated(player_id: int, elements: Array) +signal deck_empty() +signal game_ended(winner_ids: Array) +signal turn_order_determined(order: Array) +signal settlement_damage_applied(player_id: int, damage_type: DamageType, amount: int) + +# ---- 枚举 ---- +enum GamePhase { + PREPARATION, ## 游戏准备阶段 + ACTION1, ## 行动阶段一(防御一般伤害物 + 攻击/辅助/回复) + SETTLEMENT1, ## 结算阶段一(结算一般伤害物) + DRAW, ## 摸牌阶段 + ACTION2, ## 行动阶段二(攻击/辅助/回复 + 防御腐蚀/中毒) + SETTLEMENT2, ## 结算阶段二(结算腐蚀+中毒 + 弃牌检查 + 清除无害气体/水) + DYING_ENTER, ## 进入濒死阶段 + DYING_REPLY, ## 濒死回复阶段 + DYING_SETTLE, ## 濒死结算阶段 + ELIMINATION, ## 出局流程 + GAME_END, ## 游戏结束 +} + +enum CardType { + ATOM, ## 原子牌(如 Fe, O, H, Cl 等) + ATOM_GROUP, ## 原子团牌(如 SO4^2-, OH-, CO3^2- 等) + CONDITION, ## 条件牌(点燃·升温、溶解·降温、过滤·集气、提取·结晶) + SEQUENCE, ## 定序牌(甲乙丙丁戊己) + ELECTRON_LAYER, ## 电子层牌 + COUNT, ## 计数牌 +} + +enum DamageType { + CORROSION, ## 腐蚀伤害物(强酸、强碱、可溶性弱酸、可溶性弱碱) + POISONING, ## 中毒伤害物(毒气、有毒纯液体、氰化物等) + GENERAL, ## 一般伤害物(难溶性酸碱盐、广义完全水解固体、固体单质、固体氧化物) +} + +enum ConditionEffect { + IGNITE, ## 点燃:作为反应条件或否决降温,默认自带氧气 + HEAT, ## 升温:作为反应条件或否决降温/降温效果 + DISSOLVE, ## 溶解:自带水,使反应区内一种与水反应的物质发生与水的反应 + COOL, ## 降温:否决升温/点燃效果 + FILTER, ## 过滤:除去自己反应区内所有难溶性酸碱盐或广义完全水解固体或固体单质或固体氧化物 + COLLECT_GAS, ## 集气:除去自己反应区内所有气体 + EXTRACT, ## 提取:使除自己以外的玩家区域中一种非生成物物质或一个元素脱离原区域进入自己手牌 + CRYSTALLIZE, ## 结晶:从弃牌堆选择任意至多两张卡牌组成常温下能以晶体形式存在的物质,明牌加入手牌 +} + +enum OperationType { + ATTACK, ## 进攻 + DEFEND, ## 防御 + REPLY, ## 回复 + ASSIST, ## 辅助 + DARK_ATTACK, ## 暗攻 + START_INJURY, ## 起伤 + SUPPLEMENT_INJURY, ## 补伤 + PREVENT_INJURY, ## 防伤 + DISPATCH, ## 调度 + COMMUNICATION, ## 通讯 +} + +# ---- 网络相关 ---- var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new() - -var max_player_num: int +var max_player_num: int = 4 var player_num: int = 0 -var players_id: Array[int] -var players_data: Dictionary[int, Dictionary] -var reaction_area: Dictionary[int, Array] -var card_order: Array[int] -var deck_cards: Dictionary[String, int] -var deck_cards_keys: Array[String] = deck_cards.keys() -var max_hp: int +var players_id: Array[int] = [] -var game_started: bool -var game_round: int +# ---- 游戏核心数据 ---- +var players_data: Dictionary = {} # [peer_id: Dictionary] +var current_phase: GamePhase = GamePhase.PREPARATION +var current_player_id: int = 1 +var game_round: int = 0 +var game_started: bool = false +var turn_order: Array[int] = [] # 出牌顺序(天干顺序排列的peer_id) +var max_electron_layers: int = 3 # 电子层上限(建议2或3) + +# ---- 牌堆 ---- +var deck: Array[String] = [] ## 牌堆(游戏牌) +var discard_pile: Array[String] = [] ## 弃牌堆 +var sequence_cards: Array[String] = ["甲", "乙", "丙", "丁", "戊", "己"] + +# ---- 濒死相关 ---- +var dying_player_id: int = -1 +var dying_attacker_id: int = -1 ## 造成最后直接伤害的来源玩家 + +# ---- 回合内操作限制 ---- +var _dispatch_used_this_turn: Dictionary = {} # [peer_id: bool] +var _communication_used_this_turn: Dictionary = {} # [peer_id: bool] + +# ---- 内部辅助 ---- +var _substance_timestamp: int = 0 ## 物质打出时间戳,用于后置先发结算顺序 + + +## ============================================================ +## 网络连接管理 +## ============================================================ func create_server(playern: int) -> void: max_player_num = playern player_num = 1 if peer.create_server(GameManager.port, playern) != OK: + print("Failed to create server on port: ", GameManager.port) return setup_multiplayer() add_player(1) - + func create_client(ip: String) -> void: if peer.create_client(ip, GameManager.port) != OK: + print("Failed to connect to server: ", ip) return setup_multiplayer() @@ -34,6 +130,16 @@ func setup_multiplayer() -> void: multiplayer.multiplayer_peer = peer multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) + multiplayer.connected_to_server.connect(_on_connected_to_server) + multiplayer.connection_failed.connect(_on_connection_failed) + + +func _on_connected_to_server() -> void: + print("Connected to server successfully!") + + +func _on_connection_failed() -> void: + print("Failed to connect to server") func _on_peer_connected(id: int) -> void: @@ -41,9 +147,11 @@ func _on_peer_connected(id: int) -> void: add_player(id) -func _on_peer_disconnected(id: int): +func _on_peer_disconnected(id: int) -> void: if multiplayer.is_server() and players_data.has(id): remove_player(id) + if id == current_player_id and game_started: + advance_to_next_player() func add_player(id: int) -> void: @@ -53,80 +161,1014 @@ func add_player(id: int) -> void: players_data[id] = { "id": id, "nickname": "Player%d" % [player_num], - "cards": [], - "hp": max_hp, + "electron_layers": max_electron_layers, + "max_electron_layers": max_electron_layers, + "hand": [], + "reaction_area": [], + "element_area": [], + "is_alive": true, + "is_dying": false, + "team_id": -1, + "sequence_card": "", } - reaction_area[id] = [] players_id.append(id) func remove_player(id: int) -> void: player_num -= 1 players_data.erase(id) - reaction_area.erase(id) players_id.erase(id) -func server_start_game() -> void: - if player_num <= max_player_num: - return +## ============================================================ +## 游戏准备阶段 +## ============================================================ + +func start_game() -> void: if multiplayer.is_server(): - game_round = 1 + if players_id.size() < 2: + print("Need at least 2 players to start game") + return + + game_started = true + game_round = 0 + current_phase = GamePhase.PREPARATION + + # 1. 确定出牌顺序:抽取定序牌,按天干顺序排列 + _determine_turn_order() + + # 2. 分发电子层牌,确定电子层上限 + _setup_electron_layers() + + # 3. 初始化牌堆 + _initialize_deck() + + # 4. 获取初始手牌:从甲开始依次摸取8张牌 + _deal_initial_cards() + + # 广播游戏开始 client_start_game.rpc() + + # 开始第一个玩家的回合 + game_round = 1 + current_player_id = turn_order[0] + _reset_turn_limits() + set_phase(GamePhase.ACTION1, current_player_id) + + +## 确定出牌顺序 +func _determine_turn_order() -> void: + var available_sequences: Array[String] = [] + for i in range(players_id.size()): + available_sequences.append(sequence_cards[i]) + available_sequences.shuffle() + for i in range(players_id.size()): + players_data[players_id[i]]["sequence_card"] = available_sequences[i] + + var order_map: Dictionary = {} + for id in players_id: + order_map[players_data[id]["sequence_card"]] = id + + turn_order = [] + for seq in sequence_cards: + if order_map.has(seq): + turn_order.append(order_map[seq]) + + turn_order_determined.emit(turn_order) + + +## 设置电子层 +func _setup_electron_layers() -> void: + for id in players_id: + players_data[id]["electron_layers"] = max_electron_layers + players_data[id]["max_electron_layers"] = max_electron_layers + + +## 初始化牌堆 +func _initialize_deck() -> void: + deck = [] + if GameManager.card_list: + for card_id in GameManager.card_list: + var count: int = 4 + if GameManager.card_list[card_id] is Dictionary: + count = GameManager.card_list[card_id].get("count", 4) + for i in range(count): + deck.append(card_id) + deck.shuffle() + discard_pile = [] + + +## 发放初始手牌(8张) +func _deal_initial_cards() -> void: + for id in turn_order: + for i in range(8): + var card: String = _draw_from_deck() + if card == "": + deck_empty.emit() + return + players_data[id]["hand"].append(card) + hand_updated.emit(id, players_data[id]["hand"]) + + +## ============================================================ +## 回合流程管理 +## ============================================================ @rpc("any_peer", "call_local", "reliable") func client_start_game() -> void: - pass + game_started = true + game_round = 1 +## 设置当前阶段 +func set_phase(phase: GamePhase, player_id: int) -> void: + current_phase = phase + current_player_id = player_id + game_phase_changed.emit(phase, player_id) + + match phase: + GamePhase.SETTLEMENT1: + _settle_phase1() + GamePhase.DRAW: + _draw_phase() + GamePhase.SETTLEMENT2: + _settle_phase2() + GamePhase.DYING_ENTER: + _enter_dying(player_id) + GamePhase.ELIMINATION: + _elimination_process(player_id) + + +## 行动阶段一结束,进入结算阶段一 +func end_action1() -> void: + if not multiplayer.is_server(): return + if current_phase != GamePhase.ACTION1: return + set_phase(GamePhase.SETTLEMENT1, current_player_id) + + +## 结算阶段一:结算一般伤害物 +func _settle_phase1() -> void: + if not multiplayer.is_server(): return + + for id in players_id: + if not players_data[id]["is_alive"]: continue + _settle_general_damage(id) + + _check_dying_after_settlement() + + if dying_player_id == -1: + set_phase(GamePhase.DRAW, current_player_id) + + +## 摸牌阶段 +func _draw_phase() -> void: + if not multiplayer.is_server(): return + + var draw_count: int = 4 + # 甲、乙第一回合摸牌3张 + if game_round == 1: + var player_index: int = turn_order.find(current_player_id) + if player_index <= 1: + draw_count = 3 + + for i in range(draw_count): + var card: String = _draw_from_deck() + if card == "": + deck_empty.emit() + return + players_data[current_player_id]["hand"].append(card) + + hand_updated.emit(current_player_id, players_data[current_player_id]["hand"]) + set_phase(GamePhase.ACTION2, current_player_id) + + +## 行动阶段二结束,进入结算阶段二 +func end_action2() -> void: + if not multiplayer.is_server(): return + if current_phase != GamePhase.ACTION2: return + set_phase(GamePhase.SETTLEMENT2, current_player_id) + + +## 结算阶段二:依次结算腐蚀伤害物和中毒伤害物 +func _settle_phase2() -> void: + if not multiplayer.is_server(): return + + # 先结算腐蚀伤害物 + for id in players_id: + if not players_data[id]["is_alive"]: continue + _settle_corrosion_damage(id) + + # 再结算中毒伤害物 + for id in players_id: + if not players_data[id]["is_alive"]: continue + _settle_poisoning_damage(id) + + _check_dying_after_settlement() + + if dying_player_id == -1: + _check_hand_limits() + _clear_harmless_gas_and_water() + advance_to_next_player() + + +## 结算一般伤害物:反应区剩余n种一般伤害物时,扣n层电子 +func _settle_general_damage(id: int) -> void: + var damage_count: int = 0 + var to_discard: Array = [] + + var reaction_area: Array = players_data[id]["reaction_area"] + var sorted_substances: Array = _sort_by_timestamp_desc(reaction_area) + + for substance in sorted_substances: + if substance.get("damage_type", -1) == DamageType.GENERAL: + damage_count += 1 + to_discard.append(substance) + + if damage_count > 0: + _apply_electron_loss(id, damage_count) + settlement_damage_applied.emit(id, DamageType.GENERAL, damage_count) + + for substance in to_discard: + _remove_substance_from_reaction_area(id, substance) + _discard_substance(substance) + + reaction_area_updated.emit(id, players_data[id]["reaction_area"]) + + +## 结算腐蚀伤害物:强酸碱扣2n层,弱酸碱扣n层 +func _settle_corrosion_damage(id: int) -> void: + var strong_damage: int = 0 + var weak_damage: int = 0 + var to_discard: Array = [] + + var reaction_area: Array = players_data[id]["reaction_area"] + var sorted_substances: Array = _sort_by_timestamp_desc(reaction_area) + + for substance in sorted_substances: + var dmg_type: int = substance.get("damage_type", -1) + if dmg_type == DamageType.CORROSION: + var is_strong: bool = substance.get("is_strong_acid_base", false) + if is_strong: + strong_damage += 1 + else: + weak_damage += 1 + to_discard.append(substance) + + var total_loss: int = 2 * strong_damage + weak_damage + if total_loss > 0: + _apply_electron_loss(id, total_loss) + settlement_damage_applied.emit(id, DamageType.CORROSION, total_loss) + + for substance in to_discard: + _remove_substance_from_reaction_area(id, substance) + _discard_substance(substance) + + reaction_area_updated.emit(id, players_data[id]["reaction_area"]) + + +## 结算中毒伤害物:剩余n个中毒伤害物时,扣n层电子 +func _settle_poisoning_damage(id: int) -> void: + var damage_count: int = 0 + var to_discard: Array = [] + + var reaction_area: Array = players_data[id]["reaction_area"] + var sorted_substances: Array = _sort_by_timestamp_desc(reaction_area) + + for substance in sorted_substances: + if substance.get("damage_type", -1) == DamageType.POISONING: + damage_count += 1 + to_discard.append(substance) + + if damage_count > 0: + _apply_electron_loss(id, damage_count) + settlement_damage_applied.emit(id, DamageType.POISONING, damage_count) + + for substance in to_discard: + _remove_substance_from_reaction_area(id, substance) + _discard_substance(substance) + + reaction_area_updated.emit(id, players_data[id]["reaction_area"]) + + +## ============================================================ +## 濒死流程 +## ============================================================ + +func _check_dying_after_settlement() -> void: + for id in players_id: + if not players_data[id]["is_alive"]: continue + if players_data[id]["electron_layers"] <= 0: + dying_player_id = id + set_phase(GamePhase.DYING_ENTER, id) + return + + +func _enter_dying(player_id: int) -> void: + players_data[player_id]["is_dying"] = true + player_dying_entered.emit(player_id) + set_phase(GamePhase.DYING_REPLY, player_id) + + +## 濒死回复阶段结束 +func end_dying_reply() -> void: + if not multiplayer.is_server(): return + set_phase(GamePhase.DYING_SETTLE, dying_player_id) + + +## 濒死结算 +func _dying_settle(player_id: int) -> void: + if players_data[player_id]["electron_layers"] > 0: + players_data[player_id]["is_dying"] = false + player_dying_resolved.emit(player_id, true) + dying_player_id = -1 + if current_phase == GamePhase.SETTLEMENT1: + set_phase(GamePhase.DRAW, current_player_id) + elif current_phase == GamePhase.SETTLEMENT2: + _check_hand_limits() + _clear_harmless_gas_and_water() + advance_to_next_player() + else: + player_dying_resolved.emit(player_id, false) + set_phase(GamePhase.ELIMINATION, player_id) + + +## ============================================================ +## 出局流程 +## ============================================================ + +func _elimination_process(player_id: int) -> void: + if not multiplayer.is_server(): return + + players_data[player_id]["is_alive"] = false + players_data[player_id]["is_dying"] = false + + # 出局玩家将手牌和反应区内所有牌置入弃牌堆 + for card_id in players_data[player_id]["hand"]: + discard_pile.append(card_id) + players_data[player_id]["hand"] = [] + + for substance in players_data[player_id]["reaction_area"]: + _discard_substance(substance) + players_data[player_id]["reaction_area"] = [] + + for card_id in players_data[player_id]["element_area"]: + discard_pile.append(card_id) + players_data[player_id]["element_area"] = [] + + # 攻击者增益:造成最后直接伤害来源的玩家立即摸三张牌 + if dying_attacker_id != -1 and players_data.has(dying_attacker_id) and players_data[dying_attacker_id]["is_alive"]: + for i in range(3): + var card: String = _draw_from_deck() + if card == "": break + players_data[dying_attacker_id]["hand"].append(card) + hand_updated.emit(dying_attacker_id, players_data[dying_attacker_id]["hand"]) + + player_eliminated.emit(player_id, dying_attacker_id) + dying_player_id = -1 + dying_attacker_id = -1 + + if _check_game_end(): + set_phase(GamePhase.GAME_END, -1) + else: + if current_phase == GamePhase.SETTLEMENT1: + set_phase(GamePhase.DRAW, current_player_id) + elif current_phase == GamePhase.SETTLEMENT2: + _check_hand_limits() + _clear_harmless_gas_and_water() + advance_to_next_player() + + +## ============================================================ +## 游戏操作 +## ============================================================ + +## 进攻:将伤害性物质打入目标玩家反应区 +@rpc("any_peer", "call_local", "reliable") +func attack_play_substance(attacker_id: int, target_id: int, substance_data: Dictionary) -> void: + if not _is_valid_turn(attacker_id): return + if not players_data[target_id]["is_alive"]: return + + if substance_data.get("is_illegal_attack", false): + print("Illegal attack substance cannot be directly played") + return + + _substance_timestamp += 1 + substance_data["source_player_id"] = attacker_id + substance_data["timestamp"] = _substance_timestamp + + players_data[target_id]["reaction_area"].append(substance_data) + + var used_cards: Array = substance_data.get("card_ids", []) + for card_id in used_cards: + players_data[attacker_id]["hand"].erase(card_id) + + reaction_area_updated.emit(target_id, players_data[target_id]["reaction_area"]) + hand_updated.emit(attacker_id, players_data[attacker_id]["hand"]) + + +## 防御:在反应区中打出物质使伤害性物质发生反应 +@rpc("any_peer", "call_local", "reliable") +func defend_play_substance(defender_id: int, substance_data: Dictionary, target_substance_indices: Array) -> void: + if not _is_valid_turn(defender_id): return + + var reaction_area: Array = players_data[defender_id]["reaction_area"] + var defended_substances: Array = [] + for idx in target_substance_indices: + if idx >= 0 and idx < reaction_area.size(): + defended_substances.append(reaction_area[idx]) + + for substance in defended_substances: + _discard_substance(substance) + reaction_area.erase(substance) + + _discard_substance(substance_data) + + var used_cards: Array = substance_data.get("card_ids", []) + for card_id in used_cards: + players_data[defender_id]["hand"].erase(card_id) + + reaction_area_updated.emit(defender_id, players_data[defender_id]["reaction_area"]) + hand_updated.emit(defender_id, players_data[defender_id]["hand"]) + + +## 使用条件牌否定对方的条件牌 +@rpc("any_peer", "call_local", "reliable") +func negate_condition(player_id: int, condition_card_id: String, target_condition_index: int, target_player_id: int) -> void: + var negation_map: Dictionary = { + ConditionEffect.COOL: [ConditionEffect.HEAT, ConditionEffect.IGNITE], + ConditionEffect.HEAT: [ConditionEffect.COOL], + ConditionEffect.IGNITE: [ConditionEffect.COOL], + ConditionEffect.DISSOLVE: [ConditionEffect.CRYSTALLIZE], + ConditionEffect.CRYSTALLIZE: [ConditionEffect.DISSOLVE], + } + + var my_effect: int = _get_condition_effect(condition_card_id) + var target_area: Array = players_data[target_player_id]["reaction_area"] + + if target_condition_index < 0 or target_condition_index >= target_area.size(): + return + + var target_substance: Dictionary = target_area[target_condition_index] + var target_conditions: Array = target_substance.get("condition_effects", []) + + var can_negate: bool = false + if negation_map.has(my_effect): + for target_effect in target_conditions: + if my_effect in negation_map.get(target_effect, []): + can_negate = true + break + + if not can_negate: + print("Cannot negate: no valid negation relationship") + return + + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + for target_effect in target_conditions: + if my_effect in negation_map.get(target_effect, []): + target_conditions.erase(target_effect) + break + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + reaction_area_updated.emit(target_player_id, players_data[target_player_id]["reaction_area"]) + + +## 回复:打出H2O回复一电子层 +@rpc("any_peer", "call_local", "reliable") +func reply_play_water(player_id: int, card_ids: Array) -> void: + if not _is_valid_turn(player_id): return + if players_data[player_id]["electron_layers"] < players_data[player_id]["max_electron_layers"]: + players_data[player_id]["electron_layers"] += 1 + for card_id in card_ids: + players_data[player_id]["hand"].erase(card_id) + discard_pile.append(card_id) + player_data_updated.emit(player_id) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## O2特殊操作 +@rpc("any_peer", "call_local", "reliable") +func play_o2(player_id: int, choice: int, target_id: int = -1) -> void: + if not _is_valid_turn(player_id): return + + var o2_cards: Array = _find_o2_in_hand(player_id) + if o2_cards.size() < 2: + return + + for card_id in o2_cards: + players_data[player_id]["hand"].erase(card_id) + discard_pile.append(card_id) + + if choice == 0 and target_id != -1: + if players_data[target_id]["electron_layers"] < players_data[target_id]["max_electron_layers"]: + players_data[target_id]["electron_layers"] += 1 + player_data_updated.emit(target_id) + elif choice == 1: + for i in range(3): + var card: String = _draw_from_deck() + if card == "": break + players_data[player_id]["hand"].append(card) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## 起伤:在对方元素区放入元素 +@rpc("any_peer", "call_local", "reliable") +func start_injury(player_id: int, target_id: int, card_id: String) -> void: + if not _is_valid_turn(player_id): return + if not players_data[target_id]["is_alive"]: return + if players_data[target_id]["element_area"].size() >= 2: + print("Element area is full (max 2)") + return + + players_data[player_id]["hand"].erase(card_id) + players_data[target_id]["element_area"].append(card_id) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + element_area_updated.emit(target_id, players_data[target_id]["element_area"]) + + +## 补伤:将元素区的元素补充完整为伤害性物质 +@rpc("any_peer", "call_local", "reliable") +func supplement_injury(player_id: int, target_id: int, supplement_card_id: String, substance_data: Dictionary) -> void: + if not _is_valid_turn(player_id): return + if not players_data[target_id]["is_alive"]: return + + var element_area: Array = players_data[target_id]["element_area"] + var used_elements: Array = substance_data.get("element_card_ids", []) + for elem_id in used_elements: + element_area.erase(elem_id) + + players_data[player_id]["hand"].erase(supplement_card_id) + + _substance_timestamp += 1 + substance_data["source_player_id"] = player_id + substance_data["timestamp"] = _substance_timestamp + players_data[target_id]["reaction_area"].append(substance_data) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + element_area_updated.emit(target_id, players_data[target_id]["element_area"]) + reaction_area_updated.emit(target_id, players_data[target_id]["reaction_area"]) + + +## 防伤:将友方元素区的元素补充完整为伤害性物质,直接进入弃牌堆 +@rpc("any_peer", "call_local", "reliable") +func prevent_injury(player_id: int, target_id: int, supplement_card_id: String, substance_data: Dictionary) -> void: + if not _is_valid_turn(player_id): return + + var element_area: Array = players_data[target_id]["element_area"] + var used_elements: Array = substance_data.get("element_card_ids", []) + for elem_id in used_elements: + element_area.erase(elem_id) + + players_data[player_id]["hand"].erase(supplement_card_id) + _discard_substance(substance_data) + discard_pile.append(supplement_card_id) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + element_area_updated.emit(target_id, players_data[target_id]["element_area"]) + + +## 暗攻:对其他玩家反应区打出非伤害性物质 +@rpc("any_peer", "call_local", "reliable") +func dark_attack(player_id: int, target_id: int, substance_data: Dictionary) -> void: + if not _is_valid_turn(player_id): return + if not players_data[target_id]["is_alive"]: return + + _substance_timestamp += 1 + substance_data["source_player_id"] = player_id + substance_data["timestamp"] = _substance_timestamp + substance_data["is_dark_attack"] = true + + players_data[target_id]["reaction_area"].append(substance_data) + + var used_cards: Array = substance_data.get("card_ids", []) + for card_id in used_cards: + players_data[player_id]["hand"].erase(card_id) + + reaction_area_updated.emit(target_id, players_data[target_id]["reaction_area"]) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## 辅助:对其他反应区内有物质的玩家补充打出物质 +@rpc("any_peer", "call_local", "reliable") +func assist_attack(player_id: int, target_id: int, substance_data: Dictionary) -> void: + if not _is_valid_turn(player_id): return + if not players_data[target_id]["is_alive"]: return + + _substance_timestamp += 1 + substance_data["source_player_id"] = player_id + substance_data["timestamp"] = _substance_timestamp + + players_data[target_id]["reaction_area"].append(substance_data) + + var used_cards: Array = substance_data.get("card_ids", []) + for card_id in used_cards: + players_data[player_id]["hand"].erase(card_id) + + dying_attacker_id = player_id + + reaction_area_updated.emit(target_id, players_data[target_id]["reaction_area"]) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## 使用条件牌 +@rpc("any_peer", "call_local", "reliable") +func use_condition_card(player_id: int, condition_card_id: String, effect: int, target_id: int = -1) -> void: + if not _is_valid_turn(player_id): return + + if effect == ConditionEffect.FILTER: + if target_id != player_id: return + _remove_filterable_substances(player_id, condition_card_id) + elif effect == ConditionEffect.COLLECT_GAS: + if target_id != player_id: return + _remove_all_gases(player_id, condition_card_id) + elif effect == ConditionEffect.EXTRACT: + if target_id == player_id: return + _extract_from_player(player_id, target_id, condition_card_id) + elif effect == ConditionEffect.CRYSTALLIZE: + _crystallize_from_discard(player_id, condition_card_id) + elif effect in [ConditionEffect.IGNITE, ConditionEffect.HEAT, ConditionEffect.DISSOLVE]: + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## 调度:每回合限一次 +@rpc("any_peer", "call_local", "reliable") +func dispatch(player_id: int, base_card_id: String, same_kind_card_ids: Array) -> void: + if not multiplayer.is_server(): return + if not _is_valid_turn(player_id): return + if _dispatch_used_this_turn.get(player_id, false): return + + _dispatch_used_this_turn[player_id] = true + + players_data[player_id]["hand"].erase(base_card_id) + discard_pile.append(base_card_id) + + var n: int = min(same_kind_card_ids.size(), 3) + for i in range(n): + var card_id: String = same_kind_card_ids[i] + players_data[player_id]["hand"].erase(card_id) + discard_pile.append(card_id) + + for i in range(n - 1): + var card: String = _draw_from_deck() + if card == "": break + players_data[player_id]["hand"].append(card) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## 通讯:每回合限一次 +@rpc("any_peer", "call_local", "reliable") +func communication(player_id: int, card_id: String, teammate_id: int) -> void: + if not _is_valid_turn(player_id): return + if _communication_used_this_turn.get(player_id, false): return + + _communication_used_this_turn[player_id] = true + print("Player %d shows card %s to teammate %d" % [player_id, card_id, teammate_id]) + + +## 防御时弃牌视为打出H2O +@rpc("any_peer", "call_local", "reliable") +func defend_with_water(player_id: int, discarded_card_id: String) -> void: + if not _is_valid_turn(player_id): return + players_data[player_id]["hand"].erase(discarded_card_id) + discard_pile.append(discarded_card_id) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +## ============================================================ +## 辅助方法 +## ============================================================ + +func _draw_from_deck() -> String: + if deck.size() == 0: + return "" + return deck.pop_front() + + +func _apply_electron_loss(player_id: int, amount: int) -> void: + players_data[player_id]["electron_layers"] -= amount + if players_data[player_id]["electron_layers"] <= 0: + dying_attacker_id = _find_last_attacker(player_id) + player_data_updated.emit(player_id) + + +func _find_last_attacker(player_id: int) -> int: + var reaction_area: Array = players_data[player_id]["reaction_area"] + if reaction_area.size() > 0: + var sorted: Array = _sort_by_timestamp_desc(reaction_area) + for substance in sorted: + if substance.get("is_harmful", false): + return substance.get("source_player_id", -1) + return -1 + + +func _sort_by_timestamp_desc(substances: Array) -> Array: + var sorted: Array = substances.duplicate() + sorted.sort_custom(func(a, b): return a.get("timestamp", 0) > b.get("timestamp", 0)) + return sorted + + +func _remove_substance_from_reaction_area(player_id: int, substance: Dictionary) -> void: + players_data[player_id]["reaction_area"].erase(substance) + + +func _discard_substance(substance: Dictionary) -> void: + var card_ids: Array = substance.get("card_ids", []) + for card_id in card_ids: + discard_pile.append(card_id) + var condition_ids: Array = substance.get("condition_card_ids", []) + for cond_id in condition_ids: + discard_pile.append(cond_id) + + +func _check_hand_limits() -> void: + for id in players_id: + if not players_data[id]["is_alive"]: continue + var hand_limit: int = players_data[id]["electron_layers"] + 4 + while players_data[id]["hand"].size() > hand_limit: + var card: String = players_data[id]["hand"].pop_back() + discard_pile.append(card) + hand_updated.emit(id, players_data[id]["hand"]) + + +func _clear_harmless_gas_and_water() -> void: + for id in players_id: + if not players_data[id]["is_alive"]: continue + var to_remove: Array = [] + for substance in players_data[id]["reaction_area"]: + if substance.get("is_harmless_gas", false) or substance.get("is_generated_water", false): + to_remove.append(substance) + for substance in to_remove: + _remove_substance_from_reaction_area(id, substance) + _discard_substance(substance) + if to_remove.size() > 0: + reaction_area_updated.emit(id, players_data[id]["reaction_area"]) + + +func _remove_filterable_substances(player_id: int, condition_card_id: String) -> void: + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + var to_remove: Array = [] + for substance in players_data[player_id]["reaction_area"]: + if substance.get("damage_type", -1) == DamageType.GENERAL: + to_remove.append(substance) + for substance in to_remove: + _remove_substance_from_reaction_area(player_id, substance) + _discard_substance(substance) + + reaction_area_updated.emit(player_id, players_data[player_id]["reaction_area"]) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +func _remove_all_gases(player_id: int, condition_card_id: String) -> void: + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + var to_remove: Array = [] + for substance in players_data[player_id]["reaction_area"]: + if substance.get("is_gas", false): + to_remove.append(substance) + for substance in to_remove: + _remove_substance_from_reaction_area(player_id, substance) + _discard_substance(substance) + + reaction_area_updated.emit(player_id, players_data[player_id]["reaction_area"]) + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +func _extract_from_player(player_id: int, target_id: int, condition_card_id: String) -> void: + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + if players_data[target_id]["reaction_area"].size() > 0: + var substance: Dictionary = players_data[target_id]["reaction_area"][0] + if not substance.get("is_product", false): + _remove_substance_from_reaction_area(target_id, substance) + var card_ids: Array = substance.get("card_ids", []) + for card_id in card_ids: + players_data[player_id]["hand"].append(card_id) + reaction_area_updated.emit(target_id, players_data[target_id]["reaction_area"]) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +func _crystallize_from_discard(player_id: int, condition_card_id: String) -> void: + players_data[player_id]["hand"].erase(condition_card_id) + discard_pile.append(condition_card_id) + + if discard_pile.size() > 0: + var card: String = discard_pile.pop_back() + players_data[player_id]["hand"].append(card) + + hand_updated.emit(player_id, players_data[player_id]["hand"]) + + +func _get_condition_effect(card_id: String) -> int: + if GameManager.card_list.has(card_id) and GameManager.card_list[card_id] is Dictionary: + return GameManager.card_list[card_id].get("condition_effect", -1) + var condition_name_map: Dictionary = { + "点燃·升温": ConditionEffect.IGNITE, + "升温·点燃": ConditionEffect.HEAT, + "溶解·降温": ConditionEffect.DISSOLVE, + "降温·溶解": ConditionEffect.COOL, + "过滤·集气": ConditionEffect.FILTER, + "集气·过滤": ConditionEffect.COLLECT_GAS, + "提取·结晶": ConditionEffect.EXTRACT, + "结晶·提取": ConditionEffect.CRYSTALLIZE, + } + for key in condition_name_map: + if card_id.find(key) != -1: + return condition_name_map[key] + return -1 + + +func _find_o2_in_hand(player_id: int) -> Array: + var o_cards: Array = [] + for card_id in players_data[player_id]["hand"]: + if _get_card_type(card_id) == CardType.ATOM and _get_card_element(card_id) == "O": + o_cards.append(card_id) + if o_cards.size() >= 2: + return o_cards + return [] + + +func _get_card_type(card_id: String) -> int: + if GameManager.card_list.has(card_id) and GameManager.card_list[card_id] is Dictionary: + return GameManager.card_list[card_id].get("type", CardType.ATOM) + return CardType.ATOM + + +func _get_card_element(card_id: String) -> String: + if GameManager.card_list.has(card_id) and GameManager.card_list[card_id] is Dictionary: + return GameManager.card_list[card_id].get("element", "") + return "" + + +func _is_valid_turn(player_id: int) -> bool: + if not game_started: return false + if not players_data.has(player_id): return false + if not players_data[player_id]["is_alive"]: return false + if players_data[player_id]["is_dying"] and current_phase in [GamePhase.DYING_ENTER, GamePhase.DYING_REPLY]: + return true + if current_phase in [GamePhase.ACTION1, GamePhase.ACTION2]: + return player_id == current_player_id + if current_phase in [GamePhase.ACTION1, GamePhase.ACTION2, GamePhase.DYING_REPLY]: + return true + return false + + +func _reset_turn_limits() -> void: + _dispatch_used_this_turn = {} + _communication_used_this_turn = {} + + +## 切换到下一个玩家 +func advance_to_next_player() -> void: + if not multiplayer.is_server(): return + + var current_index: int = turn_order.find(current_player_id) + if current_index == -1: + for i in range(turn_order.size()): + var next_id: int = turn_order[i] + if players_data[next_id]["is_alive"]: + current_player_id = next_id + break + else: + var next_index: int = current_index + for _i in range(turn_order.size()): + next_index = (next_index + 1) % turn_order.size() + if players_data[turn_order[next_index]]["is_alive"]: + current_player_id = turn_order[next_index] + break + + game_round += 1 + _reset_turn_limits() + set_phase(GamePhase.ACTION1, current_player_id) + + +## 检查游戏是否结束 +func _check_game_end() -> bool: + var alive_count: int = 0 + var alive_ids: Array[int] = [] + for id in players_id: + if players_data[id]["is_alive"]: + alive_count += 1 + alive_ids.append(id) + + if alive_count <= 1: + game_ended.emit(alive_ids) + return true + return false + + +## 结束当前回合 +@rpc("any_peer", "call_local", "reliable") +func end_current_phase() -> void: + var peer_id: int = multiplayer.get_remote_sender_id() + if peer_id == 0: peer_id = multiplayer.get_unique_id() + + match current_phase: + GamePhase.ACTION1: + end_action1() + GamePhase.ACTION2: + end_action2() + GamePhase.DYING_REPLY: + end_dying_reply() + + +## ============================================================ +## 物质数据创建 +## ============================================================ + +func create_substance_data(formula: String, card_ids: Array, damage_type: int = -1, is_harmful: bool = false, is_illegal_attack: bool = false, is_strong_acid_base: bool = false, is_gas: bool = false, is_harmless_gas: bool = false, is_generated_water: bool = false, condition_card_ids: Array = [], condition_effects: Array = []) -> Dictionary: + return { + "formula": formula, + "card_ids": card_ids, + "damage_type": damage_type, + "is_harmful": is_harmful, + "is_illegal_attack": is_illegal_attack, + "is_strong_acid_base": is_strong_acid_base, + "is_gas": is_gas, + "is_harmless_gas": is_harmless_gas, + "is_generated_water": is_generated_water, + "condition_card_ids": condition_card_ids, + "condition_effects": condition_effects, + "source_player_id": -1, + "timestamp": 0, + "is_product": false, + "is_dark_attack": false, + } + + +## ============================================================ +## 公共查询接口 +## ============================================================ + +func get_player_data(player_id: int) -> Dictionary: + if players_data.has(player_id): + return players_data[player_id] + return {} + +func get_alive_players() -> Array[int]: + var alive: Array[int] = [] + for id in players_id: + if players_data[id]["is_alive"]: + alive.append(id) + return alive + +func get_current_player() -> int: + return current_player_id + +func get_current_phase() -> GamePhase: + return current_phase + +func get_game_round() -> int: + return game_round + +func get_turn_order() -> Array[int]: + return turn_order + +func get_deck_size() -> int: + return deck.size() + +func get_discard_pile() -> Array[String]: + return discard_pile + +## 旧接口兼容 +var deck_cards: Dictionary[String, int] = {} +var deck_cards_keys: Array[String] = [] +var max_hp: int = max_electron_layers + func random_card() -> String: - if deck_cards.size() == 0: - return "" - var card: String = deck_cards_keys[randi_range(0, deck_cards_keys.size() - 1)] - deck_cards[card] -= 1 - if deck_cards[card] <= 0: - deck_cards.erase(card) - return card - + return _draw_from_deck() func setup_deal_card() -> void: - for id in players_id: - for i in range(8): - var card: String = random_card() - if card == "": return - players_data[id]["cards"].append(card) - + _deal_initial_cards() func round_deal_card(id: int) -> void: - if card_order.find(id) == -1: return - if game_round == 1 and card_order.find(id) <= 2: - for _i in range(3): - var card: String = random_card() - if card == "": return - players_data[id]["cards"].append(card) - else: - for _i in range(2): - var card: String = random_card() - if card == "": return - players_data[id]["cards"].append(card) - - -func reaction_id(id: int) -> bool: - - return false - - -func reaction_settle() -> bool: - - return false - - -func settle_round() -> void: - while reaction_settle(): - pass - game_round += 1 + if turn_order.find(id) == -1: return + var draw_count: int = 4 + if game_round == 1 and turn_order.find(id) <= 1: + draw_count = 3 + for _i in range(draw_count): + var card: String = _draw_from_deck() + if card == "": return + players_data[id]["hand"].append(card) +func server_start_game() -> void: + start_game() func is_game_ended() -> bool: - return false + var alive_count: int = 0 + for id in players_id: + if players_data[id]["is_alive"]: + alive_count += 1 + return alive_count <= 1 \ No newline at end of file diff --git a/scripts/game/card.gd b/scripts/game/card.gd index 88fcd4a..2edef01 100644 --- a/scripts/game/card.gd +++ b/scripts/game/card.gd @@ -1,24 +1,418 @@ extends Node2D -var description: String -var type: String -var card_name: String +## ============================================================ +## 纸片化学 - 卡牌脚本 +## card.gd - 单张卡牌的显示、交互和数据管理 +## ============================================================ + +# ---- 信号 ---- +signal card_clicked(card: Node2D) +signal card_hovered(card: Node2D) +signal card_unhovered(card: Node2D) +signal card_drag_started(card: Node2D) +signal card_drag_ended(card: Node2D, position: Vector2) +signal card_double_clicked(card: Node2D) + +# ---- 卡牌数据 ---- +var card_id: String = "" +var card_name: String = "" +var description: String = "" +var card_type: int = 0 +var element_symbol: String = "" +var atom_group_formula: String = "" +var condition_effect_primary: int = -1 +var condition_effect_secondary: int = -1 +var card_kind: String = "" + +# ---- 显示状态 ---- +var is_face_up: bool = true +var is_selected: bool = false +var is_dragging: bool = false +var is_highlighted: bool = false +var is_in_hand: bool = true +var is_in_reaction_area: bool = false +var is_in_element_area: bool = false +var original_position: Vector2 = Vector2.ZERO +var card_owner_id: int = -1 + +# ---- 显示参数 ---- +var card_width: float = 120.0 +var card_height: float = 180.0 +var card_scale_normal: float = 1.0 +var card_scale_hover: float = 1.1 +var card_scale_selected: float = 1.15 +var card_scale_dragging: float = 1.2 + +# ---- 颜色配置 ---- +var color_atom_metal: Color = Color(0.75, 0.75, 0.82, 1.0) +var color_atom_nonmetal: Color = Color(0.2, 0.6, 0.8, 1.0) +var color_atom_group: Color = Color(0.9, 0.7, 0.2, 1.0) +var color_condition: Color = Color(0.85, 0.45, 0.2, 1.0) +var color_sequence: Color = Color(0.6, 0.3, 0.7, 1.0) +var color_count: Color = Color(0.5, 0.5, 0.5, 1.0) +var color_selected: Color = Color(1.0, 1.0, 0.3, 0.5) +var color_highlight: Color = Color(0.3, 1.0, 0.3, 0.3) +var color_back: Color = Color(0.15, 0.15, 0.25, 1.0) -func set_texture(pic: String) -> void: - var image: Image = Image.new() - image.load(pic) - $Sprite.texture = ImageTexture.create_from_image(image) +func _ready() -> void: + _setup_clickable_area() + visible = false + + +func _setup_clickable_area() -> void: + if not has_node("ClickableArea"): + var area: Area2D = Area2D.new() + area.name = "ClickableArea" + add_child(area) + + var collision: CollisionShape2D = CollisionShape2D.new() + var shape: RectangleShape2D = RectangleShape2D.new() + shape.size = Vector2(card_width, card_height) + collision.shape = shape + collision.position = Vector2(card_width / 2.0, card_height / 2.0) + area.add_child(collision) + + area.input_event.connect(_on_clickable_area_input_event) + area.mouse_entered.connect(_on_mouse_entered) + area.mouse_exited.connect(_on_mouse_exited) func set_card(cname: String) -> void: + card_id = cname card_name = cname - set_texture("user://download/sources/%s/assets/pics/%s" % [DownloadManager.uuid, GameManager.pic_list[card_name]]) + + if GameManager.card_list.has(cname) and GameManager.card_list[cname] is Dictionary: + var data: Dictionary = GameManager.card_list[cname] + description = data.get("description", "") + card_type = data.get("type", GameLoopManager.CardType.ATOM) + element_symbol = data.get("element", "") + atom_group_formula = data.get("formula", "") + condition_effect_primary = data.get("condition_effect_primary", -1) + condition_effect_secondary = data.get("condition_effect_secondary", -1) + card_kind = data.get("kind", "") + else: + _infer_card_type(cname) + + set_texture("user://download/sources/%s/assets/pics/%s" % [DownloadManager.uuid, GameManager.pic_list.get(cname, "")]) + update_display() + visible = true -func set_pos(x: int, y: int) -> void: - $Sprite.position = Vector2(x, y) +func _infer_card_type(cname: String) -> void: + if cname.find("点燃") != -1 or cname.find("升温") != -1 or cname.find("降温") != -1 or cname.find("溶解") != -1 or cname.find("过滤") != -1 or cname.find("集气") != -1 or cname.find("提取") != -1 or cname.find("结晶") != -1: + card_type = GameLoopManager.CardType.CONDITION + card_kind = "condition" + elif cname in ["甲", "乙", "丙", "丁", "戊", "己"]: + card_type = GameLoopManager.CardType.SEQUENCE + elif cname.find("^") != -1 or (cname.find("-") != -1 and cname.find("·") == -1): + card_type = GameLoopManager.CardType.ATOM_GROUP + card_kind = "atom_group" + else: + card_type = GameLoopManager.CardType.ATOM + element_symbol = cname + var metals: Array = ["Li", "Na", "K", "Ca", "Ba", "Mg", "Al", "Zn", "Fe", "Cu", "Ag", "Au", "Sn", "Pb", "Mn", "Cr", "Ni", "Co", "Hg", "Ti"] + if cname in metals: + card_kind = "metal" + else: + card_kind = "nonmetal" + + +func update_display() -> void: + _update_card_color() + _update_labels() + _update_selection_display() + + +func _update_card_color() -> void: + if not is_face_up: + _set_background_color(color_back) + return + match card_type: + GameLoopManager.CardType.ATOM: + if card_kind == "metal": + _set_background_color(color_atom_metal) + else: + _set_background_color(color_atom_nonmetal) + GameLoopManager.CardType.ATOM_GROUP: + _set_background_color(color_atom_group) + GameLoopManager.CardType.CONDITION: + _set_background_color(color_condition) + GameLoopManager.CardType.SEQUENCE: + _set_background_color(color_sequence) + GameLoopManager.CardType.COUNT: + _set_background_color(color_count) + _: + _set_background_color(Color.WHITE) + + +func _set_background_color(color: Color) -> void: + if has_node("Background"): + get_node("Background").color = color + + +func _update_labels() -> void: + if not is_face_up: + if has_node("NameLabel"): + get_node("NameLabel").text = "" + if has_node("DescriptionLabel"): + get_node("DescriptionLabel").text = "" + return + + if has_node("NameLabel"): + var name_label: Label = get_node("NameLabel") + name_label.text = card_name + name_label.visible = true + + if has_node("DescriptionLabel"): + var desc_label: Label = get_node("DescriptionLabel") + desc_label.text = description + desc_label.visible = description != "" + + if has_node("ElementLabel") and card_type == GameLoopManager.CardType.ATOM: + var elem_label: Label = get_node("ElementLabel") + elem_label.text = element_symbol + elem_label.visible = element_symbol != "" + + if has_node("FormulaLabel") and card_type == GameLoopManager.CardType.ATOM_GROUP: + var formula_label: Label = get_node("FormulaLabel") + formula_label.text = atom_group_formula + formula_label.visible = atom_group_formula != "" + + if has_node("EffectLabel") and card_type == GameLoopManager.CardType.CONDITION: + var effect_label: Label = get_node("EffectLabel") + var effects: Array = [] + if condition_effect_primary != -1: + effects.append(GameLoopManager.ConditionEffect.keys()[condition_effect_primary]) + if condition_effect_secondary != -1: + effects.append(GameLoopManager.ConditionEffect.keys()[condition_effect_secondary]) + effect_label.text = "·".join(effects) + effect_label.visible = effects.size() > 0 + + +func _update_selection_display() -> void: + if is_selected: + scale = Vector2(card_scale_selected, card_scale_selected) + if has_node("SelectionHighlight"): + get_node("SelectionHighlight").visible = true + elif is_dragging: + scale = Vector2(card_scale_dragging, card_scale_dragging) + if has_node("SelectionHighlight"): + get_node("SelectionHighlight").visible = false + elif is_highlighted: + scale = Vector2(card_scale_hover, card_scale_hover) + if has_node("HighlightOverlay"): + get_node("HighlightOverlay").visible = true + else: + scale = Vector2(card_scale_normal, card_scale_normal) + if has_node("SelectionHighlight"): + get_node("SelectionHighlight").visible = false + if has_node("HighlightOverlay"): + get_node("HighlightOverlay").visible = false + + +func set_texture(pic: String) -> void: + if pic == "" or pic == null: return + var image: Image = Image.new() + if image.load(pic) == OK: + if has_node("Sprite"): + $Sprite.texture = ImageTexture.create_from_image(image) + $Sprite.position = Vector2(card_width / 2.0, card_height / 2.0) + else: + if has_node("NameLabel"): + get_node("NameLabel").text = card_name + + +func set_pos(x: float, y: float) -> void: + position = Vector2(x, y) func get_pos() -> Vector2: - return $Sprite.position + return position + + +func set_center_pos(x: float, y: float) -> void: + position = Vector2(x - card_width / 2.0, y - card_height / 2.0) + + +func get_center_pos() -> Vector2: + return position + Vector2(card_width / 2.0, card_height / 2.0) + + +func set_face_up(face_up: bool) -> void: + is_face_up = face_up + update_display() + + +func set_selected(selected: bool) -> void: + is_selected = selected + _update_selection_display() + if selected: + card_clicked.emit(self) + + +func set_highlighted(highlighted: bool) -> void: + is_highlighted = highlighted + _update_selection_display() + + +func _on_clickable_area_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void: + if not visible or not is_face_up: return + + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed: + if event.double_click: + card_double_clicked.emit(self) + else: + _start_drag(event) + set_selected(not is_selected) + else: + _end_drag(event) + elif event is InputEventMouseMotion: + if is_dragging: + _update_drag_position(event) + + +func _on_mouse_entered() -> void: + if not visible: return + card_hovered.emit(self) + if not is_selected and not is_dragging: + scale = Vector2(card_scale_hover, card_scale_hover) + if is_in_hand: + position.y -= 20.0 + + +func _on_mouse_exited() -> void: + if not visible: return + card_unhovered.emit(self) + if not is_selected and not is_dragging: + scale = Vector2(card_scale_normal, card_scale_normal) + if is_in_hand and not is_dragging: + position.y += 20.0 + + +func _start_drag(event: InputEvent) -> void: + if not is_in_hand: return + is_dragging = true + original_position = position + card_drag_started.emit(self) + z_index = 100 + _update_selection_display() + + +func _update_drag_position(event: InputEvent) -> void: + if not is_dragging: return + var global_mouse: Vector2 = event.global_position + position = get_parent().get_global_transform().affine_inverse() * global_mouse - Vector2(card_width / 2.0, card_height / 2.0) + + +func _end_drag(event: InputEvent) -> void: + if not is_dragging: return + is_dragging = false + z_index = 0 + _update_selection_display() + card_drag_ended.emit(self, position) + + +func return_to_original_position() -> void: + position = original_position + scale = Vector2(card_scale_normal, card_scale_normal) + + +func is_metal_atom() -> bool: + return card_type == GameLoopManager.CardType.ATOM and card_kind == "metal" + + +func is_nonmetal_atom() -> bool: + return card_type == GameLoopManager.CardType.ATOM and card_kind == "nonmetal" + + +func is_atom_group() -> bool: + return card_type == GameLoopManager.CardType.ATOM_GROUP + + +func is_condition() -> bool: + return card_type == GameLoopManager.CardType.CONDITION + + +func is_sequence() -> bool: + return card_type == GameLoopManager.CardType.SEQUENCE + + +func is_count() -> bool: + return card_type == GameLoopManager.CardType.COUNT + + +func is_illegal_attack_substance() -> bool: + var no_defend_metals: Array = ["Li", "Na", "K", "Ca", "Ba"] + if card_type == GameLoopManager.CardType.ATOM and element_symbol in no_defend_metals: + return true + return false + + +func get_card_kind() -> String: + return card_kind + + +func get_card_info() -> Dictionary: + return { + "card_id": card_id, + "card_name": card_name, + "description": description, + "card_type": card_type, + "element_symbol": element_symbol, + "atom_group_formula": atom_group_formula, + "condition_effect_primary": condition_effect_primary, + "condition_effect_secondary": condition_effect_secondary, + "card_kind": card_kind, + "is_face_up": is_face_up, + "is_selected": is_selected, + "card_owner_id": card_owner_id, + } + + +func get_display_name() -> String: + if not is_face_up: + return "???" + return card_name + + +func flip_card(to_face_up: bool, duration: float = 0.3) -> void: + var tween: Tween = create_tween() + tween.tween_property(self, "scale:x", 0.0, duration / 2.0) + tween.tween_callback(func(): set_face_up(to_face_up)) + tween.tween_property(self, "scale:x", card_scale_normal, duration / 2.0) + + +func animate_to_position(target_pos: Vector2, duration: float = 0.3) -> void: + var tween: Tween = create_tween() + tween.tween_property(self, "position", target_pos, duration).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + + +func fade_in(duration: float = 0.3) -> void: + modulate.a = 0.0 + visible = true + var tween: Tween = create_tween() + tween.tween_property(self, "modulate:a", 1.0, duration) + + +func fade_out(duration: float = 0.3) -> void: + var tween: Tween = create_tween() + tween.tween_property(self, "modulate:a", 0.0, duration) + tween.tween_callback(func(): visible = false) + + +func bounce_effect() -> void: + var tween: Tween = create_tween() + tween.tween_property(self, "scale", Vector2(card_scale_hover, card_scale_hover), 0.1) + tween.tween_property(self, "scale", Vector2(card_scale_normal, card_scale_normal), 0.15) + + +func shake_effect() -> void: + var original_pos: Vector2 = position + var tween: Tween = create_tween() + for i in range(3): + tween.tween_property(self, "position", original_pos + Vector2(5.0, 0.0), 0.05) + tween.tween_property(self, "position", original_pos + Vector2(-5.0, 0.0), 0.05) + tween.tween_property(self, "position", original_pos, 0.05) \ No newline at end of file diff --git a/scripts/game/game.gd b/scripts/game/game.gd index f9d2e94..e1fea75 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -1,18 +1,34 @@ extends Node2D -var card_list: Array +## ============================================================ +## 纸片化学 - 游戏场景脚本 +## game.gd - 管理游戏UI、卡牌显示、玩家交互 +## ============================================================ + +var card_list: Array = [] +var hand_card_nodes: Array = [] +var reaction_card_nodes: Dictionary = {} +var element_card_nodes: Dictionary = {} + +var hand_card_spacing: float = 130.0 +var hand_card_y: float = 1250.0 +var hand_card_start_x: float = 530.0 +var reaction_card_spacing: float = 130.0 +var element_card_spacing: float = 130.0 + +var selected_cards: Array = [] +var is_waiting_for_target: bool = false +var current_operation: int = -1 + func _ready() -> void: init() - GameLoopManager.start_game() - var card = create_card("Oxygen") - card.show() - card.set_pos(300, 300) - print(card.get_pos()) + _connect_signals() + if GameLoopManager.game_started: + _refresh_all_ui() func init() -> void: - # 设置 UI 展示文本 if multiplayer.is_server(): $IsServerLabel.text = "GAMEUI_URHOST" else: @@ -25,11 +41,295 @@ func init() -> void: break $IPLabel.text = ipaddress $Player1/Username.text = GameManager.username + $EndPhaseButton.visible = false + $PhaseLabel.text = "" + $RoundLabel.text = "" -func create_card(card_name: String): - var card = SceneManager.Card.instantiate() +func _connect_signals() -> void: + GameLoopManager.game_phase_changed.connect(_on_game_phase_changed) + GameLoopManager.player_data_updated.connect(_on_player_data_updated) + GameLoopManager.player_eliminated.connect(_on_player_eliminated) + GameLoopManager.player_dying_entered.connect(_on_player_dying_entered) + GameLoopManager.player_dying_resolved.connect(_on_player_dying_resolved) + GameLoopManager.hand_updated.connect(_on_hand_updated) + GameLoopManager.reaction_area_updated.connect(_on_reaction_area_updated) + GameLoopManager.element_area_updated.connect(_on_element_area_updated) + GameLoopManager.game_ended.connect(_on_game_ended) + GameLoopManager.turn_order_determined.connect(_on_turn_order_determined) + GameLoopManager.settlement_damage_applied.connect(_on_settlement_damage_applied) + $EndPhaseButton.pressed.connect(_on_end_phase_button_pressed) + + +func create_card(card_name: String) -> Node2D: + var card: Node2D = SceneManager.Card.instantiate() add_child(card) card.set_card(card_name) card_list.append(card) return card + + +func create_card_at(card_name: String, x: float, y: float) -> Node2D: + var card: Node2D = create_card(card_name) + card.set_pos(x, y) + return card + + +func _refresh_all_ui() -> void: + _refresh_phase_display() + _refresh_round_display() + _refresh_all_player_data() + _refresh_hand_cards() + _refresh_all_reaction_areas() + _refresh_all_element_areas() + + +func _refresh_phase_display() -> void: + var phase: int = GameLoopManager.get_current_phase() + var phase_names: Dictionary = { + GameLoopManager.GamePhase.PREPARATION: "准备阶段", + GameLoopManager.GamePhase.ACTION1: "行动阶段一", + GameLoopManager.GamePhase.SETTLEMENT1: "结算阶段一", + GameLoopManager.GamePhase.DRAW: "摸牌阶段", + GameLoopManager.GamePhase.ACTION2: "行动阶段二", + GameLoopManager.GamePhase.SETTLEMENT2: "结算阶段二", + GameLoopManager.GamePhase.DYING_ENTER: "进入濒死", + GameLoopManager.GamePhase.DYING_REPLY: "濒死回复", + GameLoopManager.GamePhase.DYING_SETTLE: "濒死结算", + GameLoopManager.GamePhase.ELIMINATION: "出局流程", + GameLoopManager.GamePhase.GAME_END: "游戏结束", + } + $PhaseLabel.text = phase_names.get(phase, "未知阶段") + var show_end: bool = phase in [GameLoopManager.GamePhase.ACTION1, GameLoopManager.GamePhase.ACTION2, GameLoopManager.GamePhase.DYING_REPLY] + $EndPhaseButton.visible = show_end + if phase == GameLoopManager.GamePhase.ACTION1: + $EndPhaseButton.text = "结束行动一 → 结算" + elif phase == GameLoopManager.GamePhase.ACTION2: + $EndPhaseButton.text = "结束行动二 → 结算" + elif phase == GameLoopManager.GamePhase.DYING_REPLY: + $EndPhaseButton.text = "宣布不再出牌" + + +func _refresh_round_display() -> void: + $RoundLabel.text = "回合: %d" % GameLoopManager.get_game_round() + + +func _refresh_all_player_data() -> void: + var my_id: int = multiplayer.get_unique_id() + var turn_order: Array = GameLoopManager.get_turn_order() + var my_data: Dictionary = GameLoopManager.get_player_data(my_id) + if not my_data.is_empty(): + $Player1/Username.text = my_data.get("nickname", "Player1") + var el: int = my_data.get("electron_layers", 0) + var max_el: int = my_data.get("max_electron_layers", 0) + $Player1/ElectronLabel.text = "电子层: %d/%d" % [el, max_el] + if my_id == GameLoopManager.get_current_player(): + $Player1/Username.add_theme_color_override("font_color", Color.YELLOW) + else: + $Player1/Username.add_theme_color_override("font_color", Color.WHITE) + if el <= 1: + $Player1/ElectronLabel.add_theme_color_override("font_color", Color.RED) + else: + $Player1/ElectronLabel.add_theme_color_override("font_color", Color.WHITE) + var other_players: Array = [] + for pid in turn_order: + if pid != my_id: + other_players.append(pid) + if other_players.size() >= 1 and has_node("Player2"): + var p2d: Dictionary = GameLoopManager.get_player_data(other_players[0]) + if not p2d.is_empty(): + $Player2/Username.text = p2d.get("nickname", "Player2") + $Player2/ElectronLabel.text = "电子层: %d/%d" % [p2d.get("electron_layers", 0), p2d.get("max_electron_layers", 0)] + if other_players.size() >= 2 and has_node("Player3"): + var p3d: Dictionary = GameLoopManager.get_player_data(other_players[1]) + if not p3d.is_empty(): + $Player3/Username.text = p3d.get("nickname", "Player3") + $Player3/ElectronLabel.text = "电子层: %d" % p3d.get("electron_layers", 0) + if other_players.size() >= 3 and has_node("Player4"): + var p4d: Dictionary = GameLoopManager.get_player_data(other_players[2]) + if not p4d.is_empty(): + $Player4/Username.text = p4d.get("nickname", "Player4") + $Player4/ElectronLabel.text = "电子层: %d" % p4d.get("electron_layers", 0) + + +func _refresh_hand_cards() -> void: + var my_id: int = multiplayer.get_unique_id() + var data: Dictionary = GameLoopManager.get_player_data(my_id) + if data.is_empty(): return + var hand: Array = data.get("hand", []) + for cn in hand_card_nodes: + if is_instance_valid(cn): cn.queue_free() + hand_card_nodes.clear() + for i in range(hand.size()): + var card_node: Node2D = create_card(hand[i]) + card_node.set_pos(hand_card_start_x + i * hand_card_spacing, hand_card_y) + card_node.is_in_hand = true + card_node.card_owner_id = my_id + card_node.is_face_up = true + card_node.show() + hand_card_nodes.append(card_node) + card_node.card_clicked.connect(_on_hand_card_clicked) + card_node.card_double_clicked.connect(_on_hand_card_double_clicked) + card_node.card_drag_ended.connect(_on_hand_card_drag_ended) + + +func _refresh_reaction_area(player_id: int) -> void: + var data: Dictionary = GameLoopManager.get_player_data(player_id) + if data.is_empty(): return + var area: Array = data.get("reaction_area", []) + if reaction_card_nodes.has(player_id): + for cn in reaction_card_nodes[player_id]: + if is_instance_valid(cn): cn.queue_free() + reaction_card_nodes[player_id].clear() + else: + reaction_card_nodes[player_id] = [] + var panel: PanelContainer = _get_reaction_panel(player_id) + if panel == null: return + var bx: float = panel.position.x + 10.0 + var by: float = panel.position.y + 10.0 + for i in range(area.size()): + var sub: Dictionary = area[i] + var cn: Node2D = create_card(sub.get("formula", "???")) + cn.set_pos(bx + i * reaction_card_spacing, by) + cn.is_in_reaction_area = true + cn.is_face_up = true + cn.show() + reaction_card_nodes[player_id].append(cn) + + +func _refresh_all_reaction_areas() -> void: + for pid in GameLoopManager.get_turn_order(): + _refresh_reaction_area(pid) + + +func _refresh_element_area(player_id: int) -> void: + var data: Dictionary = GameLoopManager.get_player_data(player_id) + if data.is_empty(): return + var area: Array = data.get("element_area", []) + if element_card_nodes.has(player_id): + for cn in element_card_nodes[player_id]: + if is_instance_valid(cn): cn.queue_free() + element_card_nodes[player_id].clear() + else: + element_card_nodes[player_id] = [] + var panel: PanelContainer = _get_element_panel(player_id) + if panel == null: return + var bx: float = panel.position.x + 10.0 + var by: float = panel.position.y + 10.0 + for i in range(area.size()): + var cn: Node2D = create_card(area[i]) + cn.set_pos(bx + i * element_card_spacing, by) + cn.is_in_element_area = true + cn.is_face_up = true + cn.show() + element_card_nodes[player_id].append(cn) + + +func _refresh_all_element_areas() -> void: + for pid in GameLoopManager.get_turn_order(): + _refresh_element_area(pid) + + +func _get_reaction_panel(player_id: int) -> PanelContainer: + var my_id: int = multiplayer.get_unique_id() + if player_id == my_id: return $Player1/ReactionAreaPanel + var turn_order: Array = GameLoopManager.get_turn_order() + var idx: int = 0 + for pid in turn_order: + if pid != my_id: + idx += 1 + if pid == player_id: + if idx == 1 and has_node("Player2/ReactionAreaPanel"): return $Player2/ReactionAreaPanel + if idx == 2 and has_node("Player3/ReactionAreaPanel"): return $Player3/ReactionAreaPanel + if idx == 3 and has_node("Player4/ReactionAreaPanel"): return $Player4/ReactionAreaPanel + return null + + +func _get_element_panel(player_id: int) -> PanelContainer: + var my_id: int = multiplayer.get_unique_id() + if player_id == my_id: + if has_node("Player1/ElementAreaPanel"): return $Player1/ElementAreaPanel + return null + var turn_order: Array = GameLoopManager.get_turn_order() + var idx: int = 0 + for pid in turn_order: + if pid != my_id: + idx += 1 + if pid == player_id: + if idx == 1 and has_node("Player2/ElementAreaPanel"): return $Player2/ElementAreaPanel + if idx == 2 and has_node("Player3/ElementAreaPanel"): return $Player3/ElementAreaPanel + if idx == 3 and has_node("Player4/ElementAreaPanel"): return $Player4/ElementAreaPanel + return null + + +func _on_game_phase_changed(phase: int, player_id: int) -> void: + _refresh_phase_display() + _refresh_round_display() + _refresh_all_player_data() + if phase == GameLoopManager.GamePhase.DRAW: _refresh_hand_cards() + if phase in [GameLoopManager.GamePhase.SETTLEMENT1, GameLoopManager.GamePhase.SETTLEMENT2]: + _refresh_all_reaction_areas() + _refresh_all_player_data() + +func _on_player_data_updated(player_id: int) -> void: _refresh_all_player_data() + +func _on_player_eliminated(player_id: int, _attacker_id: int) -> void: + _refresh_all_player_data() + _refresh_all_reaction_areas() + _refresh_all_element_areas() + _refresh_hand_cards() + var d: Dictionary = GameLoopManager.get_player_data(player_id) + _show_notification("%s 已出局!" % d.get("nickname", "Player")) + +func _on_player_dying_entered(player_id: int) -> void: + var d: Dictionary = GameLoopManager.get_player_data(player_id) + _show_notification("%s 进入濒死!" % d.get("nickname", "Player")) + _refresh_all_player_data() + +func _on_player_dying_resolved(player_id: int, survived: bool) -> void: + var d: Dictionary = GameLoopManager.get_player_data(player_id) + _show_notification("%s %s" % [d.get("nickname", "Player"), "回复成功" if survived else "回复失败"]) + _refresh_all_player_data() + +func _on_hand_updated(player_id: int, _cards: Array) -> void: + if player_id == multiplayer.get_unique_id(): _refresh_hand_cards() + +func _on_reaction_area_updated(player_id: int, _substances: Array) -> void: _refresh_reaction_area(player_id) +func _on_element_area_updated(player_id: int, _elements: Array) -> void: _refresh_element_area(player_id) + +func _on_game_ended(winner_ids: Array) -> void: + $EndPhaseButton.visible = false + if winner_ids.size() == 1: + var d: Dictionary = GameLoopManager.get_player_data(winner_ids[0]) + _show_notification("游戏结束! %s 获胜!" % d.get("nickname", "Player")) + else: + _show_notification("游戏结束! 平局!") + +func _on_turn_order_determined(_order: Array) -> void: + _refresh_all_player_data() + _show_notification("出牌顺序已确定!") + +func _on_settlement_damage_applied(player_id: int, damage_type: int, amount: int) -> void: + var d: Dictionary = GameLoopManager.get_player_data(player_id) + var tn: Dictionary = {GameLoopManager.DamageType.CORROSION: "腐蚀", GameLoopManager.DamageType.POISONING: "中毒", GameLoopManager.DamageType.GENERAL: "一般"} + _show_notification("%s 受到%d层%s伤害!" % [d.get("nickname", "Player"), amount, tn.get(damage_type, "未知")]) + +func _on_hand_card_clicked(card: Node2D) -> void: + if card.is_selected: selected_cards.append(card) + else: selected_cards.erase(card) + +func _on_hand_card_double_clicked(card: Node2D) -> void: + print("Double clicked: ", card.card_name) + +func _on_hand_card_drag_ended(card: Node2D, _position: Vector2) -> void: + card.return_to_original_position() + +func _on_end_phase_button_pressed() -> void: + GameLoopManager.end_current_phase.rpc() + +func _show_notification(text: String) -> void: + $NotificationLabel.text = text + $NotificationLabel.visible = true + var tween: Tween = create_tween() + tween.tween_interval(3.0) + tween.tween_callback(func(): $NotificationLabel.visible = false) \ No newline at end of file