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

271 lines
8.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)