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

219 lines
7.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# consumable_system.gd —— 高级消耗品与瞬移定位系统(40250 官方 1:1 对齐)
# 对齐源码:
# - 40250 char_item.cpp:3950-4050 (AFFECT_AUTO_HP/SP_RECOVERY, 日月神水开关)
# - 40250 char_item.cpp:7326-7390 (AutoRecoveryItemProcess, 自动补充与容量耗尽)
# - 40250 affect.h:95-96 (AFFECT_AUTO_HP_RECOVERY = 534, AFFECT_AUTO_SP_RECOVERY = 535)
# - 40250 char_item.cpp 龙神药水 (71027~71030: 攻/防/血/魔法比率加成, 30 分钟)
# - 40250 char_item.cpp 卷轴 (22001 回城卷轴, 22010 地标传送卷轴)
class_name ConsumableSystem
extends RefCounted
# 日月神水 Vnum 范围(40250 官方标准)
const VNUM_SUN_ELIXIR_S := 72723 # 太阳神水 (小) - 1,000,000 HP
const VNUM_SUN_ELIXIR_M := 72724 # 太阳神水 (中) - 3,000,000 HP
const VNUM_SUN_ELIXIR_L := 72725 # 太阳神水 (大) - 5,000,000 HP
const VNUM_SUN_ELIXIR_EX := 72726 # 太阳神水 (特别) - 10,000,000 HP
const VNUM_MOON_ELIXIR_S := 72727 # 月亮神水 (小) - 100,000 SP
const VNUM_MOON_ELIXIR_M := 72728 # 月亮神水 (中) - 300,000 SP
const VNUM_MOON_ELIXIR_L := 72729 # 月亮神水 (大) - 500,000 SP
const VNUM_MOON_ELIXIR_EX := 72730 # 月亮神水 (特别) - 1,000,000 SP
# 龙神药水 Vnum
const VNUM_DRAGON_GOD_ATTACK := 71027 # 龙神攻击 (+15% 伤害)
const VNUM_DRAGON_GOD_DEFENSE := 71028 # 龙神防御 (+15% 防御)
const VNUM_DRAGON_GOD_LIFE := 71029 # 龙神生命 (+20% HP)
const VNUM_DRAGON_GOD_MANA := 71030 # 龙神法力 (+20% SP)
# 卷轴 Vnum
const VNUM_TOWN_SCROLL := 22001 # 瞬间回城卷轴
const VNUM_RETURN_SCROLL := 22010 # 地标记忆/传送卷轴
# 40250 Affect 编号
const AFFECT_AUTO_HP_RECOVERY := 534
const AFFECT_AUTO_SP_RECOVERY := 535
const DRAGON_GOD_DURATION := 1800.0 # 30 分钟持续时间
# 神水默认容量表 (Vnum -> 容量)
const ELIXIR_CAPACITIES := {
72723: 1000000,
72724: 3000000,
72725: 5000000,
72726: 10000000,
72727: 100000,
72728: 300000,
72729: 500000,
72730: 1000000,
}
## 检查是否为日月神水
static func is_elixir(vnum: int) -> bool:
return (vnum >= 72723 and vnum <= 72730)
## 检查是否为生命神水(太阳神水)
static func is_hp_elixir(vnum: int) -> bool:
return (vnum >= 72723 and vnum <= 72726)
## 检查是否为法力神水(月亮神水)
static func is_sp_elixir(vnum: int) -> bool:
return (vnum >= 72727 and vnum <= 72730)
## 检查是否为龙神特药
static func is_dragon_god_potion(vnum: int) -> bool:
return vnum in [VNUM_DRAGON_GOD_ATTACK, VNUM_DRAGON_GOD_DEFENSE, VNUM_DRAGON_GOD_LIFE, VNUM_DRAGON_GOD_MANA]
## 初始化神水数据(若插槽容量未设定)
static func init_elixir_if_needed(item_data: Dictionary) -> void:
var vnum := int(item_data.get("vnum", 0))
var sockets: Array = item_data.get("sockets", [0, 0, 0]).duplicate()
while sockets.size() < 3:
sockets.append(0)
# socket[0]: 0=未激活, 1=激活
# socket[1]: 已使用量
# socket[2]: 总容量
if int(sockets[2]) <= 0:
sockets[2] = ELIXIR_CAPACITIES.get(vnum, 1000000)
sockets[0] = 0
sockets[1] = 0
item_data["sockets"] = sockets
## 切换神水激活状态 (40250 char_item.cpp:3950)
static func toggle_elixir(item_data: Dictionary) -> Dictionary:
init_elixir_if_needed(item_data)
var sockets: Array = item_data["sockets"]
var cur_active := (int(sockets[0]) != 0)
var new_active := not cur_active
sockets[0] = 1 if new_active else 0
item_data["sockets"] = sockets
var is_hp := is_hp_elixir(int(item_data.get("vnum", 0)))
var kind := "生命" if is_hp else "法力"
var msg := ("已激活%s神水自充能!" % kind) if new_active else ("已关闭%s神水自充能。" % kind)
return {
"ok": true,
"active": new_active,
"affect_type": AFFECT_AUTO_HP_RECOVERY if is_hp else AFFECT_AUTO_SP_RECOVERY,
"msg": msg,
}
## 自充能 Tick 处理 (40250 char_item.cpp:7326 AutoRecoveryItemProcess)
## 返回消耗与恢复量,若耗尽返回 exhausted=true
static func process_elixir_recovery(item_data: Dictionary, cur_val: int, max_val: int) -> Dictionary:
init_elixir_if_needed(item_data)
var sockets: Array = item_data["sockets"]
if int(sockets[0]) == 0:
return {"recovered": 0, "exhausted": false} # 未激活
var used := int(sockets[1])
var cap := int(sockets[2])
var avail := maxi(0, cap - used)
if avail <= 0:
return {"recovered": 0, "exhausted": true}
var deficit := maxi(0, max_val - cur_val)
if deficit <= 0:
return {"recovered": 0, "exhausted": false}
var heal: int = mini(deficit, avail)
sockets[1] = used + heal
item_data["sockets"] = sockets
var is_depleted := (int(sockets[1]) >= cap)
return {
"recovered": heal,
"exhausted": is_depleted,
"remaining": maxi(0, cap - int(sockets[1])),
"capacity": cap,
}
## 使用龙神特药 (40250 char_item.cpp)
## 返回 Buff 增益配置
static func use_dragon_god_potion(vnum: int) -> Dictionary:
match vnum:
VNUM_DRAGON_GOD_ATTACK:
return {
"ok": true,
"vnum": vnum,
"name": "龙神攻击",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_ATT_BONUS",
"bonus_value": 15,
"msg": "使用了龙神攻击,攻击力提升 15%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_DEFENSE:
return {
"ok": true,
"vnum": vnum,
"name": "龙神防御",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_DEF_BONUS",
"bonus_value": 15,
"msg": "使用了龙神防御,防御力提升 15%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_LIFE:
return {
"ok": true,
"vnum": vnum,
"name": "龙神生命",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_MAX_HP_PCT",
"bonus_value": 20,
"msg": "使用了龙神生命,最大生命值提升 20%,持续 30 分钟!"
}
VNUM_DRAGON_GOD_MANA:
return {
"ok": true,
"vnum": vnum,
"name": "龙神法力",
"duration": DRAGON_GOD_DURATION,
"bonus_type": "POINT_MAX_SP_PCT",
"bonus_value": 20,
"msg": "使用了龙神法力,最大法力值提升 20%,持续 30 分钟!"
}
return {"ok": false, "msg": "未知的龙神药水"}
## 使用回城卷轴 / 地标卷轴 (40250 char_item.cpp)
static func use_warp_scroll(vnum: int, item_data: Dictionary, cur_x: float, cur_y: float, cur_map_id := 1) -> Dictionary:
if vnum == VNUM_TOWN_SCROLL:
return {
"ok": true,
"action": "TOWN_WARP",
"consumed": true,
"msg": "使用了回城卷轴,即将传送回城!"
}
elif vnum == VNUM_RETURN_SCROLL:
var sockets: Array = item_data.get("sockets", [0, 0, 0]).duplicate()
while sockets.size() < 3:
sockets.append(0)
# 若未记录坐标(socket[0] == 0),则记录当前位置,不消耗卷轴!
if int(sockets[0]) == 0 and int(sockets[1]) == 0:
sockets[0] = int(cur_x)
sockets[1] = int(cur_y)
sockets[2] = cur_map_id
item_data["sockets"] = sockets
return {
"ok": true,
"action": "RECORDED",
"consumed": false,
"coords": Vector2(cur_x, cur_y),
"map_id": cur_map_id,
"msg": "成功将当前坐标 (%d, %d) 记录到地标卷轴!" % [int(cur_x), int(cur_y)]
}
else:
# 已记录坐标:传送至记录位置并消耗卷轴
var target_x := int(sockets[0])
var target_y := int(sockets[1])
var target_map := int(sockets[2])
return {
"ok": true,
"action": "WARP_TO_COORDS",
"consumed": true,
"target_x": target_x,
"target_y": target_y,
"target_map": target_map,
"msg": "使用了地标卷轴,正在传送至 (%d, %d)" % [target_x, target_y]
}
return {"ok": false, "msg": "未知的传送卷轴"}