- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
217 lines
6.7 KiB
GDScript
217 lines
6.7 KiB
GDScript
# party_management_system.gd —— 40250 8人组队面板、经验分配与领导力光环系统 1:1
|
|
# 严格对照:
|
|
# metin2/src/server/game/src/party.h, party.cpp, char_party.cpp
|
|
# Client/Eternexus/root/uiparty.py
|
|
# ClientVS22/source/UserInterface/PythonPlayer.h (PARTY_AFFECT_*)
|
|
extends RefCounted
|
|
|
|
const PARTY_MAX_MEMBER := 8 # 官方最大 8 人队伍
|
|
|
|
# 经验分配模式 (char_party.cpp)
|
|
const EXP_MODE_LEVEL := 0 # 按等级比例分配 (NON_PARITY)
|
|
const EXP_MODE_PARITY := 1 # 均分模式 (PARITY)
|
|
|
|
# 队长战术领导力职权角色分配 (uiparty.py / PythonPlayer.h)
|
|
const ROLE_NORMAL := 0 # 普通队员
|
|
const ROLE_ATTACKER := 1 # 攻击者 (增加物理攻击力)
|
|
const ROLE_TANKER := 2 # 肉盾 (增加生命值上限)
|
|
const ROLE_BUFFER := 3 # 增益者 (增加法术/技能攻击力)
|
|
const ROLE_SKILL_MASTER := 4 # 技能大师 (减少技能冷却时间)
|
|
const ROLE_DEFENDER := 5 # 防御者 (增加防御力)
|
|
const ROLE_BERSERKER := 6 # 狂战士 (增加攻击速度)
|
|
|
|
# 角色光环属性加成表
|
|
const ROLE_BUFF_BONUSES := {
|
|
ROLE_ATTACKER: { "stat": "att_grade", "val": 30 },
|
|
ROLE_TANKER: { "stat": "max_hp", "val": 2500 },
|
|
ROLE_BUFFER: { "stat": "magic_att", "val": 25 },
|
|
ROLE_SKILL_MASTER: { "stat": "cooldown_reduction", "val": 15 },
|
|
ROLE_DEFENDER: { "stat": "def_grade", "val": 40 },
|
|
ROLE_BERSERKER: { "stat": "att_speed", "val": 15 }
|
|
}
|
|
|
|
class PartyMember:
|
|
var pid: int
|
|
var name: String
|
|
var level: int
|
|
var is_leader: bool = false
|
|
var role: int = ROLE_NORMAL
|
|
var cur_hp: int = 1000
|
|
var max_hp: int = 1000
|
|
|
|
func _init(p_pid: int, p_name: String, p_lvl: int, p_leader: bool = false) -> void:
|
|
pid = p_pid
|
|
name = p_name
|
|
level = p_lvl
|
|
is_leader = p_leader
|
|
role = ROLE_NORMAL
|
|
cur_hp = 1000
|
|
max_hp = 1000
|
|
|
|
func get_hp_percent() -> float:
|
|
if max_hp <= 0:
|
|
return 0.0
|
|
return clamp(float(cur_hp) / float(max_hp), 0.0, 1.0)
|
|
|
|
var members: Dictionary = {} # pid -> PartyMember
|
|
var leader_pid: int = -1
|
|
var exp_distribution_mode: int = EXP_MODE_PARITY
|
|
|
|
func _init(p_leader_pid: int = -1, p_leader_name: String = "", p_leader_lvl: int = 1) -> void:
|
|
members.clear()
|
|
leader_pid = -1
|
|
exp_distribution_mode = EXP_MODE_PARITY
|
|
if p_leader_pid > 0:
|
|
create_party(p_leader_pid, p_leader_name, p_leader_lvl)
|
|
|
|
# 1. 创建队伍
|
|
func create_party(p_pid: int, p_name: String, p_lvl: int) -> bool:
|
|
members.clear()
|
|
leader_pid = p_pid
|
|
var leader = PartyMember.new(p_pid, p_name, p_lvl, true)
|
|
members[p_pid] = leader
|
|
return true
|
|
|
|
# 2. 邀请/加入队员 (CParty::Join)
|
|
func add_member(p_pid: int, p_name: String, p_lvl: int) -> Dictionary:
|
|
var res := { "success": false, "error_code": "OK", "message": "" }
|
|
|
|
if members.size() >= PARTY_MAX_MEMBER:
|
|
res["error_code"] = "PARTY_FULL"
|
|
res["message"] = "队伍人数已达到 8 人上限。"
|
|
return res
|
|
|
|
if members.has(p_pid):
|
|
res["error_code"] = "ALREADY_IN_PARTY"
|
|
res["message"] = "该玩家已在队伍中。"
|
|
return res
|
|
|
|
var m = PartyMember.new(p_pid, p_name, p_lvl, false)
|
|
members[p_pid] = m
|
|
res["success"] = true
|
|
res["message"] = "%s 加入了队伍。" % p_name
|
|
return res
|
|
|
|
# 3. 踢出队员 (CParty::Quit / Expel)
|
|
func expel_member(caller_pid: int, target_pid: int) -> Dictionary:
|
|
var res := { "success": false, "error_code": "OK", "message": "" }
|
|
|
|
if caller_pid != leader_pid:
|
|
res["error_code"] = "NOT_LEADER"
|
|
res["message"] = "只有队长才能踢出队员。"
|
|
return res
|
|
|
|
if not members.has(target_pid):
|
|
res["error_code"] = "MEMBER_NOT_FOUND"
|
|
return res
|
|
|
|
if target_pid == leader_pid:
|
|
res["error_code"] = "CANNOT_EXPEL_LEADER"
|
|
res["message"] = "队长不能踢出自己。"
|
|
return res
|
|
|
|
var target_name: String = members[target_pid].name
|
|
members.erase(target_pid)
|
|
res["success"] = true
|
|
res["message"] = "%s 已被移出队伍。" % target_name
|
|
return res
|
|
|
|
# 4. 离队与队长顺延/解散 (CParty::Quit)
|
|
func leave_party(p_pid: int) -> Dictionary:
|
|
var res := { "success": false, "dissolved": false, "new_leader": -1 }
|
|
if not members.has(p_pid):
|
|
return res
|
|
|
|
var was_leader: bool = (p_pid == leader_pid)
|
|
members.erase(p_pid)
|
|
|
|
if members.is_empty():
|
|
leader_pid = -1
|
|
res["dissolved"] = true
|
|
res["success"] = true
|
|
return res
|
|
|
|
# 若队长离队,移交给第一位队员
|
|
if was_leader:
|
|
var new_lead_key: int = members.keys()[0]
|
|
leader_pid = new_lead_key
|
|
members[new_lead_key].is_leader = true
|
|
res["new_leader"] = new_lead_key
|
|
|
|
res["success"] = true
|
|
return res
|
|
|
|
# 5. 队长战术领导力角色光环配置 (CParty::SetRole)
|
|
func set_member_role(caller_pid: int, target_pid: int, role: int) -> Dictionary:
|
|
var res := { "success": false, "error_code": "OK", "message": "" }
|
|
|
|
if caller_pid != leader_pid:
|
|
res["error_code"] = "NOT_LEADER"
|
|
res["message"] = "只有队长有权分配战术光环。"
|
|
return res
|
|
|
|
if not members.has(target_pid):
|
|
res["error_code"] = "MEMBER_NOT_FOUND"
|
|
return res
|
|
|
|
# 检查角色是否已被队伍中其他成员占用 (非 ROLE_NORMAL)
|
|
if role != ROLE_NORMAL:
|
|
for k in members:
|
|
if k != target_pid and members[k].role == role:
|
|
res["error_code"] = "ROLE_ALREADY_ASSIGNED"
|
|
res["message"] = "该战术光环已被其他队员占用。"
|
|
return res
|
|
|
|
members[target_pid].role = role
|
|
res["success"] = true
|
|
res["role"] = role
|
|
res["buff"] = ROLE_BUFF_BONUSES.get(role, {})
|
|
res["message"] = "成功为队员分配战术角色。"
|
|
return res
|
|
|
|
# 6. 获取队员当前享有的领导力光环加成
|
|
func get_member_buff(p_pid: int) -> Dictionary:
|
|
if not members.has(p_pid):
|
|
return {}
|
|
var r: int = members[p_pid].role
|
|
return ROLE_BUFF_BONUSES.get(r, {})
|
|
|
|
# 7. 切换经验分配模式
|
|
func set_exp_distribution_mode(caller_pid: int, mode: int) -> bool:
|
|
if caller_pid != leader_pid:
|
|
return false
|
|
if mode == EXP_MODE_PARITY or mode == EXP_MODE_LEVEL:
|
|
exp_distribution_mode = mode
|
|
return true
|
|
return false
|
|
|
|
# 8. 队伍击杀经验结算与分发 (1:1 CParty::DistributeExp)
|
|
func distribute_exp(total_exp: int) -> Dictionary:
|
|
var distribution := {}
|
|
if members.is_empty() or total_exp <= 0:
|
|
return distribution
|
|
|
|
var count := members.size()
|
|
|
|
if exp_distribution_mode == EXP_MODE_PARITY:
|
|
# 均分模式: 每个队员获得 total_exp / count
|
|
var per_member: int = total_exp / count
|
|
for k in members:
|
|
distribution[k] = per_member
|
|
else:
|
|
# 等级权重模式: 按队员等级比例分配
|
|
var sum_levels: int = 0
|
|
for k in members:
|
|
sum_levels += members[k].level
|
|
for k in members:
|
|
var share: int = int(float(total_exp) * float(members[k].level) / float(sum_levels))
|
|
distribution[k] = share
|
|
|
|
return distribution
|
|
|
|
# 9. 同步队员生命值百分比
|
|
func sync_member_hp(p_pid: int, cur: int, max_h: int) -> void:
|
|
if members.has(p_pid):
|
|
members[p_pid].cur_hp = cur
|
|
members[p_pid].max_hp = max_h
|