- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
214 lines
6.4 KiB
GDScript
214 lines
6.4 KiB
GDScript
# mail_system.gd —— Metin2 40250 游戏内邮箱系统与附件寄送提取 1:1
|
|
# 对照 40250 服务端 mailbox.cpp, input_db.cpp, uimailbox.py
|
|
class_name MailSystem
|
|
extends RefCounted
|
|
|
|
signal mail_sent(mail_id: int, sender: String, recipient: String)
|
|
signal mail_received(mail_id: int, recipient: String, title: String)
|
|
signal unread_count_changed(recipient: String, count: int)
|
|
signal attachment_claimed(mail_id: int)
|
|
|
|
const POSTAGE_FEE := 1000 # 寄件固定邮资 1000 金币
|
|
const MAX_TITLE_LENGTH := 25 # 标题最长字符
|
|
const MAX_BODY_LENGTH := 100 # 正文最长字符
|
|
const MAIL_EXPIRATION_SECONDS := 2592000.0 # 30 天有效期 (30 * 86400)
|
|
|
|
var mails: Array = []
|
|
var next_mail_id: int = 1
|
|
|
|
# 寄送邮件
|
|
func send_mail(
|
|
sender: String,
|
|
recipient: String,
|
|
title: String,
|
|
body: String,
|
|
player_data: Dictionary,
|
|
inventory: Array,
|
|
attach_slot: int = -1,
|
|
attach_gold: int = 0,
|
|
current_time: float = 0.0
|
|
) -> Dictionary:
|
|
if recipient.strip_edges().is_empty():
|
|
return {"ok": false, "reason": "EMPTY_RECIPIENT", "msg": "收件人不能为空!"}
|
|
|
|
if title.length() > MAX_TITLE_LENGTH:
|
|
return {"ok": false, "reason": "TITLE_TOO_LONG", "msg": "邮件标题过长!"}
|
|
|
|
if body.length() > MAX_BODY_LENGTH:
|
|
return {"ok": false, "reason": "BODY_TOO_LONG", "msg": "邮件正文过长!"}
|
|
|
|
if attach_gold < 0:
|
|
return {"ok": false, "reason": "INVALID_GOLD", "msg": "附加金币数额无效!"}
|
|
|
|
# 检查玩家金币 (邮资 1000 + 附加金币)
|
|
var total_gold_cost := POSTAGE_FEE + attach_gold
|
|
var player_gold: int = int(player_data.get("gold", 0))
|
|
if player_gold < total_gold_cost:
|
|
return {
|
|
"ok": false,
|
|
"reason": "INSUFFICIENT_YANG",
|
|
"required": total_gold_cost,
|
|
"current": player_gold,
|
|
"msg": "金币不足以支付邮资与附件!需要 %d 金币。" % total_gold_cost
|
|
}
|
|
|
|
# 检查附加物品
|
|
var attached_item = null
|
|
if attach_slot >= 0:
|
|
if attach_slot >= inventory.size() or inventory[attach_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_ATTACH_ITEM", "msg": "所选格子没有物品!"}
|
|
var item = inventory[attach_slot]
|
|
if item.get("is_locked", false):
|
|
return {"ok": false, "reason": "ITEM_LOCKED", "msg": "已加锁的物品无法邮寄!"}
|
|
attached_item = item.duplicate(true)
|
|
inventory[attach_slot] = null
|
|
|
|
# 扣除金币
|
|
player_data["gold"] = player_gold - total_gold_cost
|
|
|
|
# 创建邮件
|
|
var mail_id := next_mail_id
|
|
next_mail_id += 1
|
|
|
|
var mail_entry: Dictionary = {
|
|
"id": mail_id,
|
|
"sender": sender,
|
|
"recipient": recipient,
|
|
"title": title,
|
|
"body": body,
|
|
"send_time": current_time,
|
|
"expire_time": current_time + MAIL_EXPIRATION_SECONDS,
|
|
"is_read": false,
|
|
"attached_gold": attach_gold,
|
|
"attached_item": attached_item,
|
|
"is_gold_claimed": (attach_gold == 0),
|
|
"is_item_claimed": (attached_item == null)
|
|
}
|
|
|
|
mails.append(mail_entry)
|
|
|
|
mail_sent.emit(mail_id, sender, recipient)
|
|
mail_received.emit(mail_id, recipient, title)
|
|
unread_count_changed.emit(recipient, get_unread_count(recipient))
|
|
|
|
return {
|
|
"ok": true,
|
|
"mail_id": mail_id,
|
|
"msg": "邮件已成功寄出!"
|
|
}
|
|
|
|
# 获取指定收件人的收件箱
|
|
func get_inbox(recipient: String) -> Array:
|
|
var list: Array = []
|
|
for m in mails:
|
|
if m["recipient"] == recipient:
|
|
list.append(m)
|
|
return list
|
|
|
|
# 查阅邮件
|
|
func read_mail(mail_id: int, current_player: String) -> Dictionary:
|
|
for m in mails:
|
|
if m["id"] == mail_id and m["recipient"] == current_player:
|
|
if not m["is_read"]:
|
|
m["is_read"] = true
|
|
unread_count_changed.emit(current_player, get_unread_count(current_player))
|
|
return {"ok": true, "mail": m.duplicate(true)}
|
|
return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到该邮件!"}
|
|
|
|
# 提取邮件附件 (金币与物品)
|
|
func claim_attachments(
|
|
mail_id: int,
|
|
current_player: String,
|
|
player_data: Dictionary,
|
|
inventory: Array
|
|
) -> Dictionary:
|
|
var target_mail = null
|
|
for m in mails:
|
|
if m["id"] == mail_id and m["recipient"] == current_player:
|
|
target_mail = m
|
|
break
|
|
|
|
if target_mail == null:
|
|
return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到指定邮件!"}
|
|
|
|
var claimed_gold: int = 0
|
|
var claimed_item = null
|
|
|
|
# 提取金币
|
|
if not target_mail["is_gold_claimed"] and target_mail["attached_gold"] > 0:
|
|
claimed_gold = target_mail["attached_gold"]
|
|
player_data["gold"] = int(player_data.get("gold", 0)) + claimed_gold
|
|
target_mail["attached_gold"] = 0
|
|
target_mail["is_gold_claimed"] = true
|
|
|
|
# 提取物品
|
|
if not target_mail["is_item_claimed"] and target_mail["attached_item"] != null:
|
|
# 寻找背包空位
|
|
var empty_slot := -1
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
empty_slot = i
|
|
break
|
|
|
|
if empty_slot == -1:
|
|
return {
|
|
"ok": false,
|
|
"reason": "INVENTORY_FULL",
|
|
"claimed_gold": claimed_gold,
|
|
"msg": "背包已满,无法提取物品附件!"
|
|
}
|
|
|
|
claimed_item = target_mail["attached_item"].duplicate(true)
|
|
inventory[empty_slot] = claimed_item
|
|
target_mail["attached_item"] = null
|
|
target_mail["is_item_claimed"] = true
|
|
|
|
attachment_claimed.emit(mail_id)
|
|
|
|
return {
|
|
"ok": true,
|
|
"claimed_gold": claimed_gold,
|
|
"claimed_item": claimed_item,
|
|
"msg": "附件提取成功!"
|
|
}
|
|
|
|
# 删除邮件
|
|
func delete_mail(mail_id: int, current_player: String) -> Dictionary:
|
|
for i in range(mails.size()):
|
|
var m = mails[i]
|
|
if m["id"] == mail_id and m["recipient"] == current_player:
|
|
# 检查是否有未提取的附件
|
|
if not m["is_gold_claimed"] or not m["is_item_claimed"]:
|
|
return {
|
|
"ok": false,
|
|
"reason": "HAS_UNCLAIMED_ATTACHMENTS",
|
|
"msg": "邮件内包含尚未提取的附件,无法删除!"
|
|
}
|
|
|
|
mails.remove_at(i)
|
|
unread_count_changed.emit(current_player, get_unread_count(current_player))
|
|
return {"ok": true, "msg": "邮件已删除。"}
|
|
|
|
return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到指定邮件!"}
|
|
|
|
# 获取未读邮件数量
|
|
func get_unread_count(recipient: String) -> int:
|
|
var cnt := 0
|
|
for m in mails:
|
|
if m["recipient"] == recipient and not m["is_read"]:
|
|
cnt += 1
|
|
return cnt
|
|
|
|
# 过期邮件清理 (30天自动销毁)
|
|
func cleanup_expired_mails(current_time: float) -> int:
|
|
var removed := 0
|
|
var i := mails.size() - 1
|
|
while i >= 0:
|
|
if current_time >= mails[i]["expire_time"]:
|
|
var recipient: String = mails[i]["recipient"]
|
|
mails.remove_at(i)
|
|
removed += 1
|
|
unread_count_changed.emit(recipient, get_unread_count(recipient))
|
|
i -= 1
|
|
return removed
|