- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
219 lines
6.0 KiB
GDScript
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()
|