- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
264 lines
7.5 KiB
GDScript
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
|
|
}
|