- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
271 lines
8.9 KiB
GDScript
271 lines
8.9 KiB
GDScript
# biologist_chaegirab_research_system.gd —— Metin2 40250 官方生物学者车齿木全套调查研究链与永久属性嘉奖系统 1:1
|
||
# 对照 40250 服务端 collect_quest_lv30.quest ~ collect_quest_lv94.quest, char.cpp
|
||
class_name BiologistChaegirabResearchSystem
|
||
extends RefCounted
|
||
|
||
signal sample_submitted(item_name: String, success: bool, current: int, target: int)
|
||
signal capstone_submitted(stone_name: String, stage_name: String)
|
||
signal stage_completed(stage_name: String, rewards: Dictionary)
|
||
|
||
const NPC_BIOLOGIST := 20084 # 生物学者车齿木 (Biologist Chaegirab)
|
||
const VNUM_RESEARCH_ELIXIR := 71035 # 贤者之药 (提高提交成功率至 90%)
|
||
|
||
# 40250 官方 8 大里程碑阶段全量研究任务链
|
||
const RESEARCH_STAGES: Array = [
|
||
{
|
||
"stage_id": 1,
|
||
"title": "【Lv 30 研究】兽人牙齿调查",
|
||
"min_level": 30,
|
||
"sample_vnum": 30006, # 兽人牙齿
|
||
"sample_name": "兽人牙齿",
|
||
"target_count": 10,
|
||
"soul_stone_vnum": 30220, # 兽人灵魂石
|
||
"soul_stone_name": "兽人灵魂石",
|
||
"reward_stats": {"move_speed": 10},
|
||
"desc": "永久移动速度 +10"
|
||
},
|
||
{
|
||
"stage_id": 2,
|
||
"title": "【Lv 40 研究】诅咒之书调查",
|
||
"min_level": 40,
|
||
"sample_vnum": 30047, # 诅咒之书
|
||
"sample_name": "诅咒之书",
|
||
"target_count": 15,
|
||
"soul_stone_vnum": 30221, # 诅咒灵魂石
|
||
"soul_stone_name": "诅咒灵魂石",
|
||
"reward_stats": {"cast_speed_pct": 5},
|
||
"desc": "永久施法速度 +5%"
|
||
},
|
||
{
|
||
"stage_id": 3,
|
||
"title": "【Lv 50 研究】恶魔纪念物调查",
|
||
"min_level": 50,
|
||
"sample_vnum": 30015, # 恶魔纪念物
|
||
"sample_name": "恶魔纪念物",
|
||
"target_count": 15,
|
||
"soul_stone_vnum": 30222, # 恶魔灵魂石
|
||
"soul_stone_name": "恶魔灵魂石",
|
||
"reward_stats": {"defense": 60},
|
||
"desc": "永久防御力 +60"
|
||
},
|
||
{
|
||
"stage_id": 4,
|
||
"title": "【Lv 60 研究】冰之碎片调查",
|
||
"min_level": 60,
|
||
"sample_vnum": 30050, # 冰之碎片
|
||
"sample_name": "冰之碎片",
|
||
"target_count": 20,
|
||
"soul_stone_vnum": 30223, # 冰霜灵魂石
|
||
"soul_stone_name": "冰霜灵魂石",
|
||
"reward_stats": {"attack_power": 50},
|
||
"desc": "永久物理攻击力 +50"
|
||
},
|
||
{
|
||
"stage_id": 5,
|
||
"title": "【Lv 70 研究】幽灵树枝调查",
|
||
"min_level": 70,
|
||
"sample_vnum": 30165, # 幽灵树枝
|
||
"sample_name": "幽灵树枝",
|
||
"target_count": 25,
|
||
"soul_stone_vnum": 30224, # 森林灵魂石
|
||
"soul_stone_name": "森林灵魂石",
|
||
"reward_stats": {"move_speed": 11, "damage_reduction_pct": 10},
|
||
"desc": "永久移动速度 +11, 伤害抗性 +10%"
|
||
},
|
||
{
|
||
"stage_id": 6,
|
||
"title": "【Lv 80 研究】巨灵石碑调查",
|
||
"min_level": 80,
|
||
"sample_vnum": 30166, # 巨灵石碑
|
||
"sample_name": "巨灵石碑",
|
||
"target_count": 30,
|
||
"soul_stone_vnum": 30225, # 巨灵灵魂石
|
||
"soul_stone_name": "巨灵灵魂石",
|
||
"reward_stats": {"attack_speed": 6, "attack_pct": 10},
|
||
"desc": "永久攻击速度 +6, 物理伤害 +10%"
|
||
},
|
||
{
|
||
"stage_id": 7,
|
||
"title": "【Lv 85 研究】红木灵枝调查",
|
||
"min_level": 85,
|
||
"sample_vnum": 30167, # 红木灵枝
|
||
"sample_name": "红木灵枝",
|
||
"target_count": 40,
|
||
"soul_stone_vnum": 30226, # 灵魂之石
|
||
"soul_stone_name": "灵魂之石",
|
||
"reward_stats": {"all_resistance_pct": 10},
|
||
"desc": "永久全属性抗性 +10%"
|
||
},
|
||
{
|
||
"stage_id": 8,
|
||
"title": "【Lv 90 研究】首领领主证书调查",
|
||
"min_level": 90,
|
||
"sample_vnum": 30168, # 领主证书
|
||
"sample_name": "首领领主证书",
|
||
"target_count": 50,
|
||
"soul_stone_vnum": 30227, # 首领灵魂石
|
||
"soul_stone_name": "首领灵魂石",
|
||
"reward_stats": {"boss_damage_pct": 10},
|
||
"desc": "永久对野外首领伤害 +10%"
|
||
}
|
||
]
|
||
|
||
var current_stage_index: int = 0
|
||
var current_samples: int = 0
|
||
var waiting_for_soul_stone: bool = false
|
||
var completed_stages: Array = []
|
||
var permanent_bonuses: Dictionary = {}
|
||
|
||
# 获取当前正在进行的研究任务配置
|
||
func get_current_stage() -> Dictionary:
|
||
if current_stage_index >= RESEARCH_STAGES.size():
|
||
return {} # 已全部完成
|
||
return RESEARCH_STAGES[current_stage_index].duplicate(true)
|
||
|
||
# 交付样本材料
|
||
func submit_sample(inventory: Array, slot_index: int, player_level: int, force_success: bool = false, use_elixir: bool = false) -> Dictionary:
|
||
var stage = get_current_stage()
|
||
if stage.is_empty():
|
||
return {"ok": false, "reason": "ALL_RESEARCH_COMPLETED", "msg": "所有生物学者调查研究均已登峰造极!"}
|
||
|
||
if player_level < int(stage["min_level"]):
|
||
return {
|
||
"ok": false,
|
||
"reason": "PLAYER_LEVEL_TOO_LOW",
|
||
"msg": "等级不足!开启该阶段研究需达到 %d 级。" % int(stage["min_level"])
|
||
}
|
||
|
||
if waiting_for_soul_stone:
|
||
return {
|
||
"ok": false,
|
||
"reason": "WAITING_FOR_SOUL_STONE",
|
||
"msg": "样本已全部收集齐!请交付【%s】以完成终极认证。" % stage["soul_stone_name"]
|
||
}
|
||
|
||
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var item: Dictionary = inventory[slot_index]
|
||
var item_vnum := int(item.get("vnum", 0))
|
||
if item_vnum != int(stage["sample_vnum"]):
|
||
return {
|
||
"ok": false,
|
||
"reason": "WRONG_SAMPLE_ITEM",
|
||
"msg": "车齿木摇了摇头:当前阶段需要研究的是【%s】!" % stage["sample_name"]
|
||
}
|
||
|
||
# 消耗 1 件样本
|
||
var cnt := int(item.get("count", 1))
|
||
if cnt > 1:
|
||
item["count"] = cnt - 1
|
||
else:
|
||
inventory[slot_index] = null
|
||
|
||
# 判定成功率 (基础 60%,使用贤者之药 90%)
|
||
var success_chance := 0.90 if use_elixir else 0.60
|
||
var is_success := force_success or (randf() <= success_chance)
|
||
|
||
if is_success:
|
||
current_samples += 1
|
||
sample_submitted.emit(stage["sample_name"], true, current_samples, stage["target_count"])
|
||
|
||
# 检查样本是否集齐
|
||
if current_samples >= int(stage["target_count"]):
|
||
waiting_for_soul_stone = true
|
||
return {
|
||
"ok": true,
|
||
"success": true,
|
||
"current": current_samples,
|
||
"target": stage["target_count"],
|
||
"need_soul_stone": true,
|
||
"soul_stone_name": stage["soul_stone_name"],
|
||
"msg": "样本品质极高!%d 件样本已全部收集完毕,请寻找【%s】交付!" % [current_samples, stage["soul_stone_name"]]
|
||
}
|
||
|
||
return {
|
||
"ok": true,
|
||
"success": true,
|
||
"current": current_samples,
|
||
"target": stage["target_count"],
|
||
"need_soul_stone": false,
|
||
"msg": "样本验证成功!研究进度:(%d / %d)。" % [current_samples, stage["target_count"]]
|
||
}
|
||
else:
|
||
sample_submitted.emit(stage["sample_name"], false, current_samples, stage["target_count"])
|
||
return {
|
||
"ok": true,
|
||
"success": false,
|
||
"current": current_samples,
|
||
"target": stage["target_count"],
|
||
"need_soul_stone": false,
|
||
"msg": "学者车齿木叹息:这件样本受损严重,无法用于精炼分析。"
|
||
}
|
||
|
||
# 交付关底灵魂石完成该阶段
|
||
func submit_soul_stone(inventory: Array, slot_index: int, player_stats: Dictionary) -> Dictionary:
|
||
var stage = get_current_stage()
|
||
if stage.is_empty():
|
||
return {"ok": false, "reason": "ALL_RESEARCH_COMPLETED"}
|
||
|
||
if not waiting_for_soul_stone:
|
||
return {"ok": false, "reason": "SAMPLES_NOT_COMPLETED", "msg": "样本尚未收集齐,无法交付灵魂石!"}
|
||
|
||
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
||
return {"ok": false, "reason": "INVALID_SLOT"}
|
||
|
||
var item: Dictionary = inventory[slot_index]
|
||
if int(item.get("vnum", 0)) != int(stage["soul_stone_vnum"]):
|
||
return {
|
||
"ok": false,
|
||
"reason": "WRONG_SOUL_STONE",
|
||
"msg": "需要交付的是专属的【%s】!" % stage["soul_stone_name"]
|
||
}
|
||
|
||
# 消耗灵魂石
|
||
var cnt := int(item.get("count", 1))
|
||
if cnt > 1:
|
||
item["count"] = cnt - 1
|
||
else:
|
||
inventory[slot_index] = null
|
||
|
||
# 注入永久属性加成
|
||
var rewards: Dictionary = stage.get("reward_stats", {})
|
||
for k in rewards.keys():
|
||
var val = int(rewards[k])
|
||
player_stats[k] = int(player_stats.get(k, 0)) + val
|
||
permanent_bonuses[k] = int(permanent_bonuses.get(k, 0)) + val
|
||
|
||
completed_stages.append(stage["stage_id"])
|
||
current_stage_index += 1
|
||
current_samples = 0
|
||
waiting_for_soul_stone = false
|
||
|
||
capstone_submitted.emit(stage["soul_stone_name"], stage["title"])
|
||
stage_completed.emit(stage["title"], rewards)
|
||
|
||
return {
|
||
"ok": true,
|
||
"completed_stage": stage["title"],
|
||
"rewards": rewards,
|
||
"permanent_bonuses": permanent_bonuses,
|
||
"msg": "【%s】圆满达成!永久获得属性加成:%s!" % [stage["title"], stage["desc"]]
|
||
}
|
||
|
||
# 序列化
|
||
func serialize() -> Dictionary:
|
||
return {
|
||
"current_stage_index": current_stage_index,
|
||
"current_samples": current_samples,
|
||
"waiting_for_soul_stone": waiting_for_soul_stone,
|
||
"completed_stages": completed_stages.duplicate(true),
|
||
"permanent_bonuses": permanent_bonuses.duplicate(true)
|
||
}
|
||
|
||
# 反序列化
|
||
func deserialize(data: Dictionary) -> void:
|
||
current_stage_index = int(data.get("current_stage_index", 0))
|
||
current_samples = int(data.get("current_samples", 0))
|
||
waiting_for_soul_stone = bool(data.get("waiting_for_soul_stone", false))
|
||
completed_stages = data.get("completed_stages", []).duplicate(true)
|
||
permanent_bonuses = data.get("permanent_bonuses", {}).duplicate(true)
|