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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+225
View File
@@ -0,0 +1,225 @@
# demon_tower_system.gd —— 恶魔之塔 1~9 层单机爬塔挑战循环与塔顶免费铁匠(40250 官方 1:1 对齐)
# 对齐源码:
# - 40250 server/share/locale/english/quest/deviltower_zone.quest
# - 40250 server/game/src/dungeon.cpp
# - 40250 server/game/src/char_battle.cpp
class_name DemonTowerSystem
extends RefCounted
signal floor_changed(floor_num: int, floor_name: String)
signal floor_completed(floor_num: int)
signal tower_completed
signal tower_failed(reason: String)
signal blacksmith_spawned(blacksmith_vnum: int, blacksmith_name: String)
# 核心怪物与 NPC Vnum (40250 官方标准)
const VNUM_METIN_FLOOR1 := 8015 # 坚韧之石 (1层入口封印)
const VNUM_DEMON_KING := 1091 # 恶魔王 (3层 BOSS)
const VNUM_METIN_FLOOR4 := 8016 # 恶魔封印石 (4层真伪封印石)
const VNUM_ELITE_DEMON_KING := 1092 # 精英恶魔王 (6层 BOSS)
const VNUM_METIN_DEATH := 8017 # 死亡之石 (7层)
const VNUM_DEATH_REAPER := 1093 # 死神 (9层 最终 BOSS)
# 塔顶免费铁匠 NPC
const NPC_TOWER_WEAPON_SMITH := 20074 # 恶魔之塔武器铁匠
const NPC_TOWER_ARMOR_SMITH := 20075 # 恶魔之塔防具铁匠
const NPC_TOWER_JEWELRY_SMITH := 20076 # 恶魔之塔首饰铁匠
# 核心任务道具
const VNUM_ZIN_KEY_FLOOR5 := 50084 # 解除 5 层石碑钥匙
const VNUM_ZIN_MAP_FLOOR7 := 30302 # 7 层恶魔封印地图
const VNUM_ZIN_KEY_FLOOR8 := 30304 # 8 层通往塔顶钥匙
const VNUM_DEATH_REAPER_CHEST := 50082 # 9 层死神宝箱
const TOTAL_FLOORS := 9
const FLOOR_NAMES := {
1: "第 1 层:恶魔入口",
2: "第 2 层:恶魔集结所",
3: "第 3 层:恶魔王大殿",
4: "第 4 层:迷幻封印之阵",
5: "第 5 层:五柱封印禁地",
6: "第 6 层:恶魔之塔顶峰",
7: "第 7 层:死神迷宫前厅",
8: "第 8 层:死神结界禁地",
9: "第 9 层:死神王座决战",
}
# 运行期状态
var current_floor := 1
var is_active := false
var floor_time_left := 1200.0 # 20 分钟单层时限
var floor4_real_metin_idx := -1
var floor5_unlocked_pillars := 0
var floor6_blacksmith_vnum := 0
var floor6_free_refine_used := false
func start_tower() -> void:
current_floor = 1
is_active = true
floor_time_left = 1200.0
floor4_real_metin_idx = -1
floor5_unlocked_pillars = 0
floor6_blacksmith_vnum = 0
floor6_free_refine_used = false
floor_changed.emit(1, FLOOR_NAMES[1])
func get_floor() -> int:
return current_floor
func get_floor_name() -> String:
return FLOOR_NAMES.get(current_floor, "未知层")
## 第 1 层判定:击碎封印石 8015 即可通关
func on_floor1_metin_destroyed(vnum: int) -> bool:
if current_floor != 1 or not is_active:
return false
if vnum == VNUM_METIN_FLOOR1:
_advance_floor()
return true
return false
## 第 2 层判定:清理全图怪物
func on_floor2_mobs_cleared(mob_count: int) -> bool:
if current_floor != 2 or not is_active:
return false
if mob_count <= 0:
_advance_floor()
return true
return false
## 第 3 层判定:击杀恶魔王 1091
func on_floor3_boss_killed(boss_vnum: int) -> bool:
if current_floor != 3 or not is_active:
return false
if boss_vnum == VNUM_DEMON_KING:
_advance_floor()
return true
return false
## 第 4 层判定:初始化 7 座封印石并在打破真石时通关
func setup_floor4_metins() -> void:
# 7 座封印石 (索引 0..6),随机指定 1 座为真石
floor4_real_metin_idx = randi() % 7
func on_floor4_metin_hit(stone_idx: int) -> Dictionary:
if current_floor != 4 or not is_active:
return {"ok": false, "msg": "当前不在第 4 层!"}
if stone_idx == floor4_real_metin_idx:
_advance_floor()
return {"ok": true, "is_real": true, "msg": "找到了真实的恶魔封印石!通往第 5 层的传送门开启!"}
else:
return {"ok": true, "is_real": false, "msg": "这是一座虚假的封印石,迷雾尚未散开!"}
## 第 5 层判定:使用钥匙解除 5 座恶魔石柱
func on_floor5_unlock_pillar(key_vnum: int) -> Dictionary:
if current_floor != 5 or not is_active:
return {"ok": false, "msg": "当前不在第 5 层!"}
if key_vnum != VNUM_ZIN_KEY_FLOOR5:
return {"ok": false, "msg": "钥匙不匹配!"}
floor5_unlocked_pillars += 1
var remain := 5 - floor5_unlocked_pillars
if floor5_unlocked_pillars >= 5:
_advance_floor()
return {"ok": true, "completed": true, "msg": "5 座恶魔石碑全部解除封印!传送至第 6 层!"}
else:
return {"ok": true, "completed": false, "remain": remain, "msg": "成功解除了一座石碑封印,还剩 %d 座!" % remain}
## 第 6 层判定:击杀精英恶魔王 1092 并召唤免费铁匠
func on_floor6_boss_killed(boss_vnum: int) -> Dictionary:
if current_floor != 6 or not is_active:
return {"ok": false, "msg": "当前不在第 6 层!"}
if boss_vnum == VNUM_ELITE_DEMON_KING:
# 随机召唤三位塔顶铁匠之一
var smiths := [
{"vnum": NPC_TOWER_WEAPON_SMITH, "name": "恶魔之塔武器铁匠"},
{"vnum": NPC_TOWER_ARMOR_SMITH, "name": "恶魔之塔防具铁匠"},
{"vnum": NPC_TOWER_JEWELRY_SMITH, "name": "恶魔之塔首饰铁匠"}
]
var chosen: Dictionary = smiths[randi() % smiths.size()]
floor6_blacksmith_vnum = chosen["vnum"]
floor6_free_refine_used = false
blacksmith_spawned.emit(chosen["vnum"], chosen["name"])
return {
"ok": true,
"blacksmith_vnum": chosen["vnum"],
"blacksmith_name": chosen["name"],
"msg": "击杀了精英恶魔王!塔顶现身了一位【%s】!" % chosen["name"]
}
return {"ok": false, "msg": "目标不是精英恶魔王!"}
## 第 6 层塔顶免费铁匠强化 (无需材料,单人爬塔经典福利)
func perform_tower_free_refine(item: Dictionary, item_type: int) -> Dictionary:
if current_floor != 6 or floor6_blacksmith_vnum == 0:
return {"ok": false, "code": "NO_BLACKSMITH", "msg": "塔顶铁匠尚未出现!"}
if floor6_free_refine_used:
return {"ok": false, "code": "ALREADY_USED", "msg": "恶魔之塔铁匠本次只能免费强化一次!"}
# 校验铁匠类型与物品类型匹配
if floor6_blacksmith_vnum == NPC_TOWER_WEAPON_SMITH and item_type != 1:
return {"ok": false, "code": "TYPE_MISMATCH", "msg": "武器铁匠只能强化武器!"}
if floor6_blacksmith_vnum == NPC_TOWER_ARMOR_SMITH and item_type != 2:
return {"ok": false, "code": "TYPE_MISMATCH", "msg": "防具铁匠只能强化防具!"}
floor6_free_refine_used = true
var cur_vnum := int(item.get("vnum", 0))
var new_vnum := cur_vnum + 1
item["vnum"] = new_vnum
return {
"ok": true,
"code": "SUCCESS",
"old_vnum": cur_vnum,
"new_vnum": new_vnum,
"msg": "%s】为你施展了恶魔神工,免费强化成功!装备晋升为 #%d" % [
"武器铁匠" if floor6_blacksmith_vnum == NPC_TOWER_WEAPON_SMITH else "防具铁匠",
new_vnum
]
}
## 从第 6 层前往第 7 层 (要求角色等级 >= 75)
func advance_to_floor7(player_level: int) -> Dictionary:
if current_floor != 6:
return {"ok": false, "msg": "不在第 6 层!"}
if player_level < 75:
return {"ok": false, "code": "LEVEL_TOO_LOW", "msg": "只有等级达到 75 级的真正勇者,才能踏入恶魔之塔第 7 层禁地!"}
_advance_floor()
return {"ok": true, "msg": "踏入了死神禁地第 7 层!"}
## 第 7 层判定:使用恶魔地图 30302 通关
func on_floor7_map_used(map_vnum: int) -> bool:
if current_floor != 7 or not is_active:
return false
if map_vnum == VNUM_ZIN_MAP_FLOOR7:
_advance_floor()
return true
return false
## 第 8 层判定:使用钥匙 30304 通关
func on_floor8_key_used(key_vnum: int) -> bool:
if current_floor != 8 or not is_active:
return false
if key_vnum == VNUM_ZIN_KEY_FLOOR8:
_advance_floor()
return true
return false
## 第 9 层终极对决:击杀死神 1093
func on_floor9_boss_killed(boss_vnum: int) -> Dictionary:
if current_floor != 9 or not is_active:
return {"ok": false, "msg": "当前不在第 9 层!"}
if boss_vnum == VNUM_DEATH_REAPER:
is_active = false
tower_completed.emit()
return {
"ok": true,
"code": "TOWER_CLEAR",
"drop_vnum": VNUM_DEATH_REAPER_CHEST,
"msg": "壮举!你成功斩杀了死神,恶魔之塔被彻底征服!获得了【死神宝箱】!"
}
return {"ok": false, "msg": "目标不是死神!"}
func _advance_floor() -> void:
floor_completed.emit(current_floor)
current_floor += 1
floor_time_left = 1200.0
floor_changed.emit(current_floor, FLOOR_NAMES.get(current_floor, "%d" % current_floor))