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

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