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

264 lines
7.5 KiB
GDScript

# biologist_quest_system.gd —— Metin2 40250 生物学研究家任务链与永久属性加成系统 1:1
# 对照 40250 服务端 collect_quest_lv30.quest ~ collect_quest_lv80.quest 及 char.cpp:ComputePoints
class_name BiologistQuestSystem
extends RefCounted
const NPC_CHAEGIRAB := 20084
const ITEM_SPIRAL_POTION := 71035 # 灵魂秘药 / 研究家的秘药
const STAGES: Array[Dictionary] = [
{
"id": 0,
"name": "Lv30 兽人的牙齿研究",
"required_level": 30,
"collect_vnum": 30006, # 兽人齿
"collect_name": "兽人齿",
"collect_count": 10,
"soul_stone_vnum": 30220, # 兽人灵魂石
"soul_stone_name": "金龙之灵魂石",
"reward_type": "move_speed",
"reward_value": 10,
"reward_title": "永久移动速度 +10"
},
{
"id": 1,
"name": "Lv40 诅咒之书研究",
"required_level": 40,
"collect_vnum": 30047, # 诅咒书
"collect_name": "诅咒之书",
"collect_count": 15,
"soul_stone_vnum": 30221, # 诅咒灵魂石
"soul_stone_name": "神殿之灵魂石",
"reward_type": "att_speed",
"reward_value": 5,
"reward_title": "永久攻击速度 +5"
},
{
"id": 2,
"name": "Lv50 恶魔纪念物研究",
"required_level": 50,
"collect_vnum": 30015, # 恶魔纪念物
"collect_name": "恶魔纪念物",
"collect_count": 15,
"soul_stone_vnum": 30222, # 恶魔灵魂石
"soul_stone_name": "恶魔之灵魂石",
"reward_type": "def_grade",
"reward_value": 60,
"reward_title": "永久防御力 +60"
},
{
"id": 3,
"name": "Lv60 冰球研究",
"required_level": 60,
"collect_vnum": 30050, # 冰球
"collect_name": "冰球",
"collect_count": 20,
"soul_stone_vnum": 30223, # 冰球灵魂石
"soul_stone_name": "雪山之灵魂石",
"reward_type": "att_grade",
"reward_value": 50,
"reward_title": "永久攻击力 +50"
},
{
"id": 4,
"name": "Lv70 幽灵树枝研究",
"required_level": 70,
"collect_vnum": 30165, # 幽灵树枝
"collect_name": "幽灵树枝",
"collect_count": 25,
"soul_stone_vnum": 30224, # 幽灵灵魂石
"soul_stone_name": "鬼树之灵魂石",
"reward_type": "damage_reduction",
"reward_value": 10,
"reward_title": "受到伤害永久减少 10%"
},
{
"id": 5,
"name": "Lv80 图基石板研究",
"required_level": 80,
"collect_vnum": 30166, # 图基石板
"collect_name": "图基石板",
"collect_count": 30,
"soul_stone_vnum": 30225, # 图基灵魂石
"soul_stone_name": "图基之灵魂石",
"reward_type": "att_grade_pct",
"reward_value": 10,
"reward_title": "物理攻击力永久提升 10%"
}
]
var player_level: int = 1
var current_stage_idx: int = -1 # -1: 未开始
var delivered_count: int = 0
var soul_stone_phase: bool = false
var completed_stages: Array[int] = []
var cooldown_remaining: float = 0.0
var permanent_bonuses: Dictionary = {
"move_speed": 0,
"att_speed": 0,
"def_grade": 0,
"att_grade": 0,
"damage_reduction": 0,
"att_grade_pct": 0
}
func init_for_player(level: int) -> void:
player_level = level
_check_available_stage()
func _check_available_stage() -> void:
if current_stage_idx >= 0 and current_stage_idx < STAGES.size():
return # 正在进行中
for i in range(STAGES.size()):
if i not in completed_stages:
var stage: Dictionary = STAGES[i]
if player_level >= int(stage["required_level"]):
current_stage_idx = i
delivered_count = 0
soul_stone_phase = false
break
func get_current_stage() -> Dictionary:
if current_stage_idx >= 0 and current_stage_idx < STAGES.size():
return STAGES[current_stage_idx]
return {}
func can_submit() -> bool:
return current_stage_idx >= 0 and not soul_stone_phase and cooldown_remaining <= 0.0
func submit_item(inventory: Array, use_potion: bool = false, force_success: bool = false) -> Dictionary:
if current_stage_idx < 0 or current_stage_idx >= STAGES.size():
return {"ok": false, "reason": "NO_ACTIVE_QUEST"}
if soul_stone_phase:
return {"ok": false, "reason": "NEED_SOUL_STONE"}
if cooldown_remaining > 0.0 and not use_potion:
return {"ok": false, "reason": "COOLDOWN_ACTIVE", "remaining": cooldown_remaining}
var stage: Dictionary = STAGES[current_stage_idx]
var req_vnum: int = int(stage["collect_vnum"])
# 查找收集物槽位
var item_slot := -1
for i in range(inventory.size()):
var it = inventory[i]
if it != null and int(it.get("vnum", 0)) == req_vnum:
item_slot = i
break
if item_slot == -1:
return {"ok": false, "reason": "MISSING_ITEM", "vnum": req_vnum}
# 检查秘药
var potion_consumed := false
if use_potion:
for i in range(inventory.size()):
var it = inventory[i]
if it != null and int(it.get("vnum", 0)) == ITEM_SPIRAL_POTION:
inventory[i] = null
potion_consumed = true
cooldown_remaining = 0.0
break
# 扣除收集物
inventory[item_slot] = null
# 成功判定 (默认 60%,秘药 90%)
var success_chance: float = 0.90 if potion_consumed else 0.60
var roll: float = 0.0 if force_success else randf()
var success := roll < success_chance
if not success:
cooldown_remaining = 0.0 if potion_consumed else 3600.0 # 1 小时
return {
"ok": true,
"success": false,
"delivered": delivered_count,
"target": stage["collect_count"],
"msg": "样本已损坏,未能提取有效数据!"
}
# 成功交付
delivered_count += 1
var target_count: int = int(stage["collect_count"])
if delivered_count >= target_count:
soul_stone_phase = true
return {
"ok": true,
"success": true,
"delivered": delivered_count,
"target": target_count,
"stage_completed": true,
"msg": "研究样本收集完毕!接下来请获取专属灵魂石交付!"
}
else:
cooldown_remaining = 0.0 if potion_consumed else 3600.0
return {
"ok": true,
"success": true,
"delivered": delivered_count,
"target": target_count,
"stage_completed": false,
"msg": "样本质量极佳!已记录样本数据。"
}
func submit_soul_stone(inventory: Array) -> Dictionary:
if current_stage_idx < 0 or current_stage_idx >= STAGES.size():
return {"ok": false, "reason": "NO_ACTIVE_QUEST"}
if not soul_stone_phase:
return {"ok": false, "reason": "NOT_IN_SOUL_STONE_PHASE"}
var stage: Dictionary = STAGES[current_stage_idx]
var soul_vnum: int = int(stage["soul_stone_vnum"])
var found_slot := -1
for i in range(inventory.size()):
var it = inventory[i]
if it != null and int(it.get("vnum", 0)) == soul_vnum:
found_slot = i
break
if found_slot == -1:
return {"ok": false, "reason": "MISSING_SOUL_STONE", "vnum": soul_vnum}
# 消耗灵魂石
inventory[found_slot] = null
# 结算阶段奖励
var reward_type: String = String(stage["reward_type"])
var reward_val: int = int(stage["reward_value"])
permanent_bonuses[reward_type] = int(permanent_bonuses.get(reward_type, 0)) + reward_val
completed_stages.append(current_stage_idx)
var finished_stage_id: int = current_stage_idx
current_stage_idx = -1
delivered_count = 0
soul_stone_phase = false
cooldown_remaining = 0.0
# 检查下一阶段
_check_available_stage()
return {
"ok": true,
"completed_stage": finished_stage_id,
"reward_title": stage["reward_title"],
"reward_type": reward_type,
"reward_value": reward_val,
"all_bonuses": permanent_bonuses.duplicate()
}
func get_quest_status() -> Dictionary:
return {
"player_level": player_level,
"stage_idx": current_stage_idx,
"stage_info": get_current_stage(),
"delivered": delivered_count,
"target": int(get_current_stage().get("collect_count", 0)),
"soul_stone_phase": soul_stone_phase,
"cooldown_remaining": cooldown_remaining,
"completed_stages": completed_stages,
"permanent_bonuses": permanent_bonuses
}