Files
mtgodot-poc/project/battle_arena_duel_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

219 lines
6.0 KiB
GDScript

# battle_arena_duel_system.gd —— Metin2 40250 官方武斗竞技场切磋与裁判规则 1:1
# 对照 40250 服务端 arena.cpp, arena.h, BattleArena.cpp, questlua_arena.cpp
class_name BattleArenaDuelSystem
extends RefCounted
signal duel_started(opponent_name: String, set_count: int, time_limit: float)
signal round_started(current_round: int)
signal round_ended(winner_side: String, score_a: int, score_b: int)
signal duel_finished(winner_side: String, final_score_a: int, final_score_b: int, reward: Dictionary)
signal rule_violation_intercepted(reason: String)
const NPC_REFEREE := 20017 # 比武场裁判 (Arena Referee)
const ARENA_MAP_INDEX := 112
# 擂台对立出生点 (Point A 玩家, Point B 对手)
const START_POINT_A := Vector2(100.0, 100.0)
const START_POINT_B := Vector2(200.0, 200.0)
const ARENA_BOUNDS := Rect2(80.0, 80.0, 140.0, 140.0) # 擂台活动合法矩形区域
# 决斗状态枚举
const STATE_IDLE := "IDLE"
const STATE_COUNTDOWN := "COUNTDOWN"
const STATE_FIGHTING := "FIGHTING"
const STATE_ROUND_OVER:= "ROUND_OVER"
const STATE_FINISHED := "FINISHED"
# 单机官方 4 大门派守擂试炼 NPC 导师
const OPPONENT_PRESETS: Dictionary = {
"warrior": {
"id": "warrior",
"name": "战神武者教头",
"job": "warrior",
"max_hp": 12000,
"attack_power": 450,
"defense": 200,
"skills": ["三连斩", "剑气破空"]
},
"assassin": {
"id": "assassin",
"name": "暗夜游侠神箭手",
"job": "assassin",
"max_hp": 9000,
"attack_power": 520,
"defense": 150,
"skills": ["连环毒矢", "疾影步"]
},
"sura": {
"id": "sura",
"name": "冥界暗影修罗",
"job": "sura",
"max_hp": 11000,
"attack_power": 480,
"defense": 220,
"skills": ["幽冥炎刃", "魔灵护体"]
},
"shaman": {
"id": "shaman",
"name": "雷鸣灵术萨满",
"job": "shaman",
"max_hp": 10000,
"attack_power": 400,
"defense": 180,
"skills": ["天顶奔雷", "金钟守护"]
}
}
# 比赛运行时状态
var match_state: String = STATE_IDLE
var current_round: int = 1
var max_sets: int = 3 # 3 局 2 胜
var sets_to_win: int = 2
var time_limit_per_round: float = 300.0 # 5 分钟
var round_timer: float = 300.0
var countdown_timer: float = 3.0
var score_a: int = 0 # 玩家得分
var score_b: int = 0 # 对手得分
var player_pos: Vector2 = START_POINT_A
var opponent_pos: Vector2 = START_POINT_B
var active_opponent: Dictionary = {}
var opponent_current_hp: int = 0
# 启动武斗切磋
func start_duel(opponent_id: String, sets: int = 3, time_limit: float = 300.0) -> Dictionary:
if not OPPONENT_PRESETS.has(opponent_id):
return {"ok": false, "reason": "INVALID_OPPONENT", "msg": "未知的守擂武者!"}
active_opponent = OPPONENT_PRESETS[opponent_id].duplicate(true)
max_sets = sets
sets_to_win = int(ceil(float(max_sets) / 2.0))
time_limit_per_round = time_limit
score_a = 0
score_b = 0
current_round = 1
_prepare_round()
duel_started.emit(active_opponent["name"], max_sets, time_limit_per_round)
return {
"ok": true,
"opponent": active_opponent["name"],
"max_sets": max_sets,
"sets_to_win": sets_to_win,
"pos_a": player_pos,
"pos_b": opponent_pos
}
# 准备新回合
func _prepare_round() -> void:
match_state = STATE_COUNTDOWN
countdown_timer = 3.0
round_timer = time_limit_per_round
player_pos = START_POINT_A
opponent_pos = START_POINT_B
opponent_current_hp = int(active_opponent.get("max_hp", 10000))
# 心跳逻辑推进
func update(delta: float) -> void:
if match_state == STATE_COUNTDOWN:
countdown_timer -= delta
if countdown_timer <= 0.0:
match_state = STATE_FIGHTING
round_started.emit(current_round)
elif match_state == STATE_FIGHTING:
round_timer -= delta
if round_timer <= 0.0:
# 回合超时:双方均未击倒,判定为平局或根据剩余生命决出
_handle_round_over("timeout")
# 40250 禁药规则拦截
func use_potion_check(item_vnum: int) -> bool:
if match_state == STATE_FIGHTING or match_state == STATE_COUNTDOWN:
# 27001~27006 为红蓝药水
if item_vnum >= 27001 and item_vnum <= 27006:
rule_violation_intercepted.emit("POTION_FORBIDDEN_IN_ARENA")
return false
return true
# 40250 禁马规则拦截
func mount_check() -> bool:
if match_state == STATE_FIGHTING or match_state == STATE_COUNTDOWN:
rule_violation_intercepted.emit("MOUNT_FORBIDDEN_IN_ARENA")
return false
return true
# 40250 擂台边界巡检
func check_boundary(pos: Vector2) -> bool:
if not ARENA_BOUNDS.has_point(pos):
rule_violation_intercepted.emit("OUT_OF_ARENA_BOUNDS")
return false
return true
# 击倒判定处理
func record_defeat(defeated_side: String) -> Dictionary:
if match_state != STATE_FIGHTING:
return {"ok": false, "reason": "NOT_IN_FIGHTING_STATE"}
var round_winner := ""
if defeated_side == "B": # 对手倒下,玩家得分
score_a += 1
round_winner = "A"
elif defeated_side == "A": # 玩家倒下,对手得分
score_b += 1
round_winner = "B"
return _handle_round_over(round_winner)
# 回合结束与大局胜负判定
func _handle_round_over(winner_side: String) -> Dictionary:
match_state = STATE_ROUND_OVER
round_ended.emit(winner_side, score_a, score_b)
# 检查大局是否已有胜者
if score_a >= sets_to_win or score_b >= sets_to_win or current_round >= max_sets:
match_state = STATE_FINISHED
var grand_winner := "A" if score_a > score_b else ("B" if score_b > score_a else "DRAW")
var reward: Dictionary = {}
if grand_winner == "A":
reward = {
"arena_points": 100 * sets_to_win,
"gold": 500000,
"title": "【竞技场常胜军】"
}
duel_finished.emit(grand_winner, score_a, score_b, reward)
return {
"ok": true,
"match_finished": true,
"winner": grand_winner,
"score_a": score_a,
"score_b": score_b,
"reward": reward
}
# 进入下一回合
current_round += 1
_prepare_round()
return {
"ok": true,
"match_finished": false,
"current_round": current_round,
"score_a": score_a,
"score_b": score_b
}
# 重置武斗场状态
func reset_arena() -> void:
match_state = STATE_IDLE
current_round = 1
score_a = 0
score_b = 0
active_opponent.clear()