- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
171 lines
4.4 KiB
GDScript
171 lines
4.4 KiB
GDScript
# combat_replay_snapshot_system.gd —— Metin2 40250 战斗高光战报与首领击杀瞬间快照 1:1
|
|
# 对照 40250 服务端 battle.cpp, packet.h, char_battle.cpp
|
|
class_name CombatReplaySnapshotSystem
|
|
extends RefCounted
|
|
|
|
signal combat_session_started(target_name: String)
|
|
signal combat_highlight_generated(card: Dictionary)
|
|
signal new_personal_record(target_name: String, record_type: String, new_val: int)
|
|
|
|
const MAX_STORED_CARDS := 20
|
|
|
|
var is_in_session: bool = false
|
|
var session_id: int = 1
|
|
var target_vnum: int = 0
|
|
var target_name: String = ""
|
|
var target_max_hp: int = 0
|
|
|
|
var session_start_time: float = 0.0
|
|
var elapsed_time: float = 0.0
|
|
|
|
var total_damage_dealt: int = 0
|
|
var total_damage_taken: int = 0
|
|
var total_attacks: int = 0
|
|
var total_skills_used: int = 0
|
|
var max_single_hit: int = 0
|
|
var critical_hits_count: int = 0
|
|
|
|
# 历史高光战报归档
|
|
var highlight_cards: Array = []
|
|
# 个人最高纪录:target_name -> { "best_dps": int, "fastest_time": float, "highest_hit": int }
|
|
var personal_bests: Dictionary = {}
|
|
|
|
# 开始一场战斗记录
|
|
func start_combat_session(vnum: int, tname: String, max_hp: int) -> Dictionary:
|
|
is_in_session = true
|
|
target_vnum = vnum
|
|
target_name = tname
|
|
target_max_hp = max_hp
|
|
|
|
session_start_time = Time.get_unix_time_from_system()
|
|
elapsed_time = 0.0
|
|
|
|
total_damage_dealt = 0
|
|
total_damage_taken = 0
|
|
total_attacks = 0
|
|
total_skills_used = 0
|
|
max_single_hit = 0
|
|
critical_hits_count = 0
|
|
|
|
combat_session_started.emit(target_name)
|
|
|
|
return {
|
|
"ok": true,
|
|
"session_id": session_id,
|
|
"target_name": target_name,
|
|
"target_hp": target_max_hp
|
|
}
|
|
|
|
# 记录一次攻击命中
|
|
func record_hit(dmg: int, is_crit: bool, skill_name: String = "") -> void:
|
|
if not is_in_session:
|
|
return
|
|
|
|
total_damage_dealt += dmg
|
|
total_attacks += 1
|
|
|
|
if is_crit:
|
|
critical_hits_count += 1
|
|
|
|
if not skill_name.is_empty():
|
|
total_skills_used += 1
|
|
|
|
if dmg > max_single_hit:
|
|
max_single_hit = dmg
|
|
|
|
# 记录受到的伤害
|
|
func record_damage_taken(dmg: int) -> void:
|
|
if not is_in_session:
|
|
return
|
|
total_damage_taken += dmg
|
|
|
|
# 帧心跳更新战斗时长
|
|
func update(delta: float) -> void:
|
|
if is_in_session:
|
|
elapsed_time += delta
|
|
|
|
# 结束战斗并生成结构化高光快照战报
|
|
func end_combat_session(is_kill: bool, drops: Array = []) -> Dictionary:
|
|
if not is_in_session:
|
|
return {"ok": false, "reason": "NO_ACTIVE_SESSION"}
|
|
|
|
is_in_session = false
|
|
var duration = max(0.5, elapsed_time)
|
|
var dps = int(float(total_damage_dealt) / duration)
|
|
|
|
# 战斗评级评估 (S / A / B / C)
|
|
var rank = "C"
|
|
if is_kill:
|
|
if dps >= 4000:
|
|
rank = "S"
|
|
elif dps >= 2500:
|
|
rank = "A"
|
|
elif dps >= 1200:
|
|
rank = "B"
|
|
else:
|
|
rank = "C"
|
|
|
|
var card: Dictionary = {
|
|
"session_id": session_id,
|
|
"target_vnum": target_vnum,
|
|
"target_name": target_name,
|
|
"is_kill": is_kill,
|
|
"rank": rank,
|
|
"duration": duration,
|
|
"total_damage": total_damage_dealt,
|
|
"dps": dps,
|
|
"max_single_hit": max_single_hit,
|
|
"crit_count": critical_hits_count,
|
|
"damage_taken": total_damage_taken,
|
|
"skills_used": total_skills_used,
|
|
"drops": drops.duplicate(true),
|
|
"datetime": Time.get_datetime_string_from_system()
|
|
}
|
|
|
|
session_id += 1
|
|
highlight_cards.push_front(card)
|
|
if highlight_cards.size() > MAX_STORED_CARDS:
|
|
highlight_cards.pop_back()
|
|
|
|
# 刷新个人纪录判定
|
|
_update_personal_bests(card)
|
|
|
|
combat_highlight_generated.emit(card)
|
|
return card
|
|
|
|
# 检查并更新个人生涯纪录
|
|
func _update_personal_bests(card: Dictionary) -> void:
|
|
if not card["is_kill"]:
|
|
return
|
|
|
|
var tname: String = card["target_name"]
|
|
if not personal_bests.has(tname):
|
|
personal_bests[tname] = {
|
|
"best_dps": card["dps"],
|
|
"fastest_time": card["duration"],
|
|
"highest_hit": card["max_single_hit"]
|
|
}
|
|
new_personal_record.emit(tname, "FIRST_KILL", card["dps"])
|
|
return
|
|
|
|
var pb = personal_bests[tname]
|
|
if card["dps"] > pb["best_dps"]:
|
|
pb["best_dps"] = card["dps"]
|
|
new_personal_record.emit(tname, "BEST_DPS", card["dps"])
|
|
|
|
if card["duration"] < pb["fastest_time"]:
|
|
pb["fastest_time"] = card["duration"]
|
|
new_personal_record.emit(tname, "FASTEST_TIME", int(card["duration"]))
|
|
|
|
if card["max_single_hit"] > pb["highest_hit"]:
|
|
pb["highest_hit"] = card["max_single_hit"]
|
|
new_personal_record.emit(tname, "HIGHEST_HIT", card["max_single_hit"])
|
|
|
|
# 查询最近战报
|
|
func get_recent_highlights() -> Array:
|
|
return highlight_cards
|
|
|
|
# 查询特定目标的最佳战报纪录
|
|
func get_personal_best(tname: String) -> Dictionary:
|
|
return personal_bests.get(tname, {})
|