1174 lines
38 KiB
GDScript
1174 lines
38 KiB
GDScript
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 |