Files
game/scripts/autoload/GameLoopManager.gd
T

1174 lines
38 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 4
var player_num: int = 0
var players_id: Array[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()
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:
if multiplayer.is_server():
add_player(id)
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:
if player_num >= max_player_num:
return
player_num += 1
players_data[id] = {
"id": id,
"nickname": "Player%d" % [player_num],
"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": "",
}
players_id.append(id)
func remove_player(id: int) -> void:
player_num -= 1
players_data.erase(id)
players_id.erase(id)
## ============================================================
## 游戏准备阶段
## ============================================================
func start_game() -> void:
if multiplayer.is_server():
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:
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:
return _draw_from_deck()
func setup_deal_card() -> void:
_deal_initial_cards()
func round_deal_card(id: int) -> void:
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:
var alive_count: int = 0
for id in players_id:
if players_data[id]["is_alive"]:
alive_count += 1
return alive_count <= 1