Files
mtgodot-poc/project/guild_war_observer_arena_system.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

189 lines
6.3 KiB
GDScript

# guild_war_observer_arena_system.gd —— Metin2 40250 官方公会战备观战与武斗擂台传送系统 1:1
# 100% 对照 40250 服务端 war_map.cpp, questlua_pc.cpp (warp_to_guild_war_observer_position), guild_war.cpp
class_name GuildWarObserverArenaSystem
extends RefCounted
signal observer_entered(player_id: int, war_id: int, observer_pos: Vector2)
signal observer_left(player_id: int, return_map: String, return_pos: Vector2)
signal war_ended_evacuated(war_id: int, observer_count: int)
signal observer_error(reason: String)
# 40250 官方公会战竞技场地标与地图常数
const MAP_GUILD_ARENA := 110 # 公会战擂台竞技场地图
const MAP_GUILD_FLAG_WAR := 111 # 公会夺旗战场地图
const OBSERVER_PLATFORM_POS := Vector2(2000.0, 2000.0) # 观战席高台坐标 (Start Location Index 2)
# 主城公会守卫 NPC (11001, 11003, 11005)
const NPC_TOWN_GUARD_SHINSOO := 11001
const NPC_TOWN_GUARD_CHUNJO := 11003
const NPC_TOWN_GUARD_JINNO := 11005
# 正在进行的公会战清单: war_id -> { ... }
var _active_wars: Dictionary = {}
# 观战者档案: player_id -> { "war_id": int, "saved_map": String, "saved_pos": Vector2, "is_observer": bool }
var _observers: Dictionary = {}
var _next_war_id: int = 1
# ==========================================
# 1. 公会战状态登记与告示 (war_map.cpp)
# ==========================================
func register_active_war(
guild1_id: int,
guild1_name: String,
guild2_id: int,
guild2_name: String,
war_type: int = 1, # 1 为 Arena 擂台赛
target_score: int = 50
) -> int:
var wid = _next_war_id
_next_war_id += 1
_active_wars[wid] = {
"war_id": wid,
"guild1_id": guild1_id,
"guild1_name": guild1_name,
"guild2_id": guild2_id,
"guild2_name": guild2_name,
"war_type": war_type,
"target_score": target_score,
"score1": 0,
"score2": 0,
"map_index": MAP_GUILD_ARENA if war_type == 1 else MAP_GUILD_FLAG_WAR,
"is_ended": false,
"winner_id": 0
}
return wid
# 主城守卫公告栏:获取当前全部可观战的公会对决列表
func get_active_wars_for_display() -> Array[Dictionary]:
var list: Array[Dictionary] = []
for wid in _active_wars.keys():
var w = _active_wars[wid]
if not bool(w.get("is_ended", false)):
list.append(w.duplicate(true))
return list
# 更新战局比分
func update_war_score(war_id: int, score1: int, score2: int) -> bool:
if not _active_wars.has(war_id):
return false
var w = _active_wars[war_id]
w["score1"] = score1
w["score2"] = score2
if score1 >= int(w["target_score"]):
end_war(war_id, int(w["guild1_id"]))
elif score2 >= int(w["target_score"]):
end_war(war_id, int(w["guild2_id"]))
return true
# ==========================================
# 2. 传送入场观战 (pc_warp_to_guild_war_observer_position)
# ==========================================
func enter_as_observer(
player_id: int,
war_id: int,
current_map: String,
current_pos: Vector2
) -> Dictionary:
if not _active_wars.has(war_id) or bool(_active_wars[war_id].get("is_ended", false)):
observer_error.emit("WAR_NOT_ACTIVE")
return {"ok": false, "reason": "WAR_NOT_ACTIVE", "msg": "该场公会战尚未开启或已经落下帷幕!"}
if is_player_observing(player_id):
observer_error.emit("ALREADY_OBSERVING")
return {"ok": false, "reason": "ALREADY_OBSERVING", "msg": "你已经在观战席中!"}
var war = _active_wars[war_id]
# 保存玩家原始位置 (ch->SaveExitLocation)
_observers[player_id] = {
"war_id": war_id,
"saved_map": current_map,
"saved_pos": current_pos,
"is_observer": true,
"can_attack": false, # 幽灵模式禁止攻击
"can_be_attacked": false # 幽灵模式免疫任何伤害
}
observer_entered.emit(player_id, war_id, OBSERVER_PLATFORM_POS)
return {
"ok": true,
"war_id": war_id,
"map_index": war["map_index"],
"observer_pos": OBSERVER_PLATFORM_POS,
"is_observer_mode": true,
"msg": "穿过虚空之门,你抵达了公会战竞技场高台观战席!已开启隐匿观摩形态。"
}
# 离开观战席重返主城
func leave_observer_mode(player_id: int) -> Dictionary:
if not is_player_observing(player_id):
observer_error.emit("NOT_OBSERVING")
return {"ok": false, "reason": "NOT_OBSERVING", "msg": "你当前并不在观战模式中。"}
var obs = _observers[player_id]
var return_map = str(obs["saved_map"])
var return_pos = Vector2(obs["saved_pos"].x, obs["saved_pos"].y)
_observers.erase(player_id)
observer_left.emit(player_id, return_map, return_pos)
return {
"ok": true,
"return_map": return_map,
"return_pos": return_pos,
"msg": "你退出了观战高台,平安返回了主城集结地。"
}
func is_player_observing(player_id: int) -> bool:
return _observers.has(player_id) and bool(_observers[player_id].get("is_observer", false))
func can_player_attack_or_be_attacked(player_id: int) -> bool:
# 若处于观战模式,彻底屏蔽战斗互攻判定
return not is_player_observing(player_id)
# ==========================================
# 3. 终局结算与观战席全员疏散 (war_map.cpp)
# ==========================================
func end_war(war_id: int, winner_guild_id: int) -> void:
if not _active_wars.has(war_id):
return
var w = _active_wars[war_id]
w["is_ended"] = true
w["winner_id"] = winner_guild_id
# 遣返该场战斗的所有观战者
var evacuated_count := 0
for pid in _observers.keys().duplicate():
if int(_observers[pid].get("war_id", 0)) == war_id:
var obs = _observers[pid]
observer_left.emit(pid, obs["saved_map"], obs["saved_pos"])
_observers.erase(pid)
evacuated_count += 1
war_ended_evacuated.emit(war_id, evacuated_count)
# 序列化与反序列化
func serialize() -> Dictionary:
var obs_copy = {}
for pid in _observers:
var item = _observers[pid].duplicate()
item["saved_pos"] = {"x": item["saved_pos"].x, "y": item["saved_pos"].y}
obs_copy[pid] = item
return {
"active_wars": _active_wars.duplicate(true),
"observers": obs_copy,
"next_war_id": _next_war_id
}
func deserialize(data: Dictionary) -> void:
_active_wars = data.get("active_wars", {}).duplicate(true)
_next_war_id = int(data.get("next_war_id", 1))
_observers.clear()
var raw_obs = data.get("observers", {})
for pid in raw_obs:
var item = raw_obs[pid].duplicate()
var p_dict = item.get("saved_pos", {"x": 0.0, "y": 0.0})
item["saved_pos"] = Vector2(float(p_dict.get("x", 0.0)), float(p_dict.get("y", 0.0)))
_observers[int(pid)] = item