装备校验链/快捷栏/公会/好友情侣/交易仓库五项差距修复(增量 129)
- 装备校验:can_equip 按 CanEquipNow→FindEquipCell→EquipItem 精确重排, 新增 find_equip_cell() 1:1 复刻(RING/UNIQUE 占用翻转、COSTUME/BELT/DS), 帝国 antiflag 降级为 tooltip 提示,LIMIT_PCBANG/REAL_TIME* 语义与倒计时文案 - 快捷栏:死亡全槽禁用+激活守卫、变身(affect 220)技能置灰、 物品槽失配置灰(服务器回收/换武器后刷新) - 公会:GUILD_AUTH_* 权限位门控踢人/公告/技能升级,/gskillup、 被宣战应答弹窗(/war·/nowar)、资金存取行(40250 服务端已禁用仍保留 1:1) - 好友/情侣:messenger 三固定组(好友/公会/情侣),lover_login/logout/ near/far/divorce 命令事件→在线/在身边状态与离婚隐藏 - 交易/仓库:ExchangeState.last_notice(ALREADY/LESS_GOLD)结束浮层提示; 仓库改密 /safebox_change_password 三段输入;关窗补 /safebox_close·/mall_close; 仓库金钱经核实 40250 无 wire 协议,UI 定界只读 - app_flow:重连时 guild_marks_ready 重复连接加 is_connected 守卫 - 相机 look_at 零向量守卫(首帧/传送落点) - 测试:equip/love/guild 断言更新,79 项中 78 绿(game_camera_test 为 HEAD 既有失败);真服冒烟 LIVE_SMOKE PASS(192.168.21.203 全链路) - docs/CLIENT-GAP.md 同步增量 129 Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kUFvQGYusuY3dkfGitmAe
This commit is contained in:
+4
-1
@@ -400,7 +400,10 @@ func _build_char_list() -> void:
|
||||
if not client.has_method("change_name") or not bool(client.call("change_name", idx, name)):
|
||||
if screen.has_method("on_char_name_send_failed"):
|
||||
screen.on_char_name_send_failed())
|
||||
if client.has_signal("guild_marks_ready"):
|
||||
# 重连会重建选人界面再走一遍本函数;ONE_SHOT 只有在信号触发过才自动断开,
|
||||
# 首次连接若还挂着会报 already connected——先查再连。
|
||||
if client.has_signal("guild_marks_ready") \
|
||||
and not client.guild_marks_ready.is_connected(_on_marks_ready_for_select):
|
||||
client.guild_marks_ready.connect(_on_marks_ready_for_select, CONNECT_ONE_SHOT)
|
||||
|
||||
func _forward_char_evt(a = null, sig := "") -> void:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bohd5850be0ex
|
||||
+130
-33
@@ -1,13 +1,20 @@
|
||||
# equip_rules.gd —— 装备穿戴合法性校验(纯函数校验器)。
|
||||
#
|
||||
# 语义顺序对齐服务器 `char_item.cpp::CanEquipNow`:
|
||||
# antiflag(性别 / 职业 / 帝国) → wearable_flag 与目标 wear 槽匹配 → limit(等级 / 属性)。
|
||||
# 语义顺序 1:1 对齐服务器真实链(已核对 40250 源码):
|
||||
# `char_item.cpp:7462 CanEquipNow`:JOB antiflag → limit(LEVEL/STR/INT/DEX/CON)
|
||||
# → WEARABLE_UNIQUE 同组去重(结婚戒指另需已婚,客户端不判)。
|
||||
# `char_item.cpp:6100 EquipItem`(拿到 `item.cpp:472 FindEquipCell` 的槽位后):
|
||||
# 燕尾服+骑乘禁止 → 变身中除 WEAR_ARROW 一律禁止 → `FN_check_item_sex`(性别)
|
||||
# → 骑乘物品已骑乘禁止 → 攻击/施法后 1.5s 冷却(客户端不判)。
|
||||
# **帝国 antiflag 在装备路径上从不检查**(只在掉落/商店显示层)——已移出装备门,
|
||||
# 见 empire_warning(),供 tooltip 提示用。
|
||||
# LIMIT_PCBANG / LIMIT_REAL_TIME* 不是装备门禁(item.cpp:1899 PCBANG 仅标记;
|
||||
# REAL_TIME* 由 socket0/socket1 驱动到期,客户端职责只有 tooltip 倒计时)。
|
||||
#
|
||||
# 参考枚举 1:1 自 `REF/GameLib/ItemData.h`(`EItemAntiFlag` / `EItemWearableFlag` /
|
||||
# `EWearPositions` / `ELimitTypes` / `EItemType` / `EArmorSubTypes` / `ECostumeSubTypes`)。
|
||||
# 40250 客户端本身**不做** `CanEquipNow` 前置门(`PythonItemModule` 只暴露 `IsWearableFlag` /
|
||||
# `GetLimit`,真正的仲裁在服务器)。这里按 §3.9 `__CanShot` 的方式在客户端防御式重建一道前置
|
||||
# 门——精确的 tie-break 顺序、PCBANG / REAL_TIME 类 limit、已穿同槽替换等留待接服务器比对(seam)。
|
||||
# 40250 客户端本身**不做** `CanEquipNow` 前置门(真正的仲裁在服务器)。这里按 §3.9
|
||||
# `__CanShot` 的方式在客户端防御式重建一道前置门,被拦下时给出与服务器一致的拒绝码。
|
||||
extends RefCounted
|
||||
|
||||
# --- EItemAntiFlag(ItemData.h,classic 排列:帝国位在 1<<9..1<<11)---
|
||||
@@ -65,10 +72,18 @@ const LIMIT_REAL_TIME := 7
|
||||
# --- EItemType(相关项)+ 子类 ---
|
||||
const ITEM_TYPE_WEAPON := 1
|
||||
const ITEM_TYPE_ARMOR := 2
|
||||
const ITEM_TYPE_UNIQUE := 16
|
||||
const ITEM_TYPE_TOTEM := 26
|
||||
const ITEM_TYPE_COSTUME := 28
|
||||
const ITEM_TYPE_DS := 29
|
||||
const ITEM_TYPE_SPECIAL_DS := 30
|
||||
const ITEM_TYPE_RING := 33
|
||||
const ITEM_TYPE_BELT := 34
|
||||
|
||||
# --- EUniqueSubTypes(item_length.h:骑乘/坐骑物品,服务器 IsRideItem)---
|
||||
const UNIQUE_SPECIAL_RIDE := 2
|
||||
const UNIQUE_SPECIAL_MOUNT_RIDE := 3
|
||||
|
||||
const ARMOR_BODY := 0
|
||||
const ARMOR_HEAD := 1
|
||||
const ARMOR_SHIELD := 2
|
||||
@@ -103,6 +118,8 @@ static func job_of_race(race: int) -> int:
|
||||
|
||||
|
||||
# 物品默认落在哪个 wear 槽(type / sub_type 决定)。不可穿戴 → -1。
|
||||
# 戒指无穿戴表时默认 RING1;有穿戴表请用 find_equip_cell(RING1 空则 1 否则 2,
|
||||
# 与服务器 item.cpp:472 一致)。
|
||||
static func default_wear_for(item: Dictionary) -> int:
|
||||
var itype := int(item.get("type", -1))
|
||||
var sub := int(item.get("sub_type", -1))
|
||||
@@ -163,47 +180,91 @@ static func slot_accepts(item: Dictionary, wear: int) -> bool:
|
||||
return (int(item.get("wear_flags", 0)) & need) != 0
|
||||
|
||||
|
||||
# 服务器 `item.cpp:472 CItem::FindEquipCell` 的 1:1 复刻。
|
||||
# worn : 当前穿戴表 {wear_cell: vnum}(客户端来自 get_equipment())。
|
||||
# else-if 优先级:BODY→HEAD→FOOTS→WRIST→WEAPON→SHIELD→NECK→EAR→ARROW;
|
||||
# RING→RING1 空则 RING1 否则 RING2;UNIQUE→UNIQUE1 空则 UNIQUE1 否则 UNIQUE2。
|
||||
# 龙魂石(DS/SPECIAL_DS)服务器返回 WEAR_MAX_NUM+sub,但客户端走
|
||||
# dragon_soul_ui 的专用协议路径,这里返回 -1 不进装备门。
|
||||
static func find_equip_cell(item: Dictionary, worn: Dictionary = {}) -> int:
|
||||
var itype := int(item.get("type", -1))
|
||||
var sub := int(item.get("sub_type", -1))
|
||||
var flags := int(item.get("wear_flags", 0))
|
||||
if (flags == 0 or itype == ITEM_TYPE_TOTEM) and itype != ITEM_TYPE_COSTUME \
|
||||
and itype != ITEM_TYPE_DS and itype != ITEM_TYPE_SPECIAL_DS \
|
||||
and itype != ITEM_TYPE_RING and itype != ITEM_TYPE_BELT:
|
||||
return -1
|
||||
if itype == ITEM_TYPE_DS or itype == ITEM_TYPE_SPECIAL_DS:
|
||||
return -1 # 见上:客户端 DS 专用路径
|
||||
if itype == ITEM_TYPE_COSTUME:
|
||||
return WEAR_COSTUME_HAIR if sub == COSTUME_HAIR else WEAR_COSTUME_BODY
|
||||
if itype == ITEM_TYPE_RING:
|
||||
return WEAR_RING2 if worn.has(WEAR_RING1) else WEAR_RING1
|
||||
if itype == ITEM_TYPE_BELT:
|
||||
return WEAR_BELT
|
||||
if flags & WEARABLE_BODY:
|
||||
return WEAR_BODY
|
||||
if flags & WEARABLE_HEAD:
|
||||
return WEAR_HEAD
|
||||
if flags & WEARABLE_FOOTS:
|
||||
return WEAR_FOOTS
|
||||
if flags & WEARABLE_WRIST:
|
||||
return WEAR_WRIST
|
||||
if flags & WEARABLE_WEAPON:
|
||||
return WEAR_WEAPON
|
||||
if flags & WEARABLE_SHIELD:
|
||||
return WEAR_SHIELD
|
||||
if flags & WEARABLE_NECK:
|
||||
return WEAR_NECK
|
||||
if flags & WEARABLE_EAR:
|
||||
return WEAR_EAR
|
||||
if flags & WEARABLE_ARROW:
|
||||
return WEAR_ARROW
|
||||
if flags & WEARABLE_UNIQUE:
|
||||
return WEAR_UNIQUE2 if worn.has(WEAR_UNIQUE1) else WEAR_UNIQUE1
|
||||
return -1
|
||||
|
||||
# 骑乘 / 坐骑物品(服务器 `item.cpp:1695 IsRideItem`:UNIQUE + RIDE / MOUNT_RIDE)。
|
||||
static func is_ride_item(item: Dictionary) -> bool:
|
||||
return int(item.get("type", -1)) == ITEM_TYPE_UNIQUE \
|
||||
and (int(item.get("sub_type", -1)) == UNIQUE_SPECIAL_RIDE \
|
||||
or int(item.get("sub_type", -1)) == UNIQUE_SPECIAL_MOUNT_RIDE)
|
||||
|
||||
# 帝国 antiflag:服务器装备路径不检查(显示层语义)。返回冲突的 anti 位(0 = 无冲突),
|
||||
# tooltip 用它给出提示,不作为装备门。
|
||||
static func empire_warning(item: Dictionary, empire: int) -> int:
|
||||
var bit := int(EMPIRE_ANTIFLAG.get(empire, 0))
|
||||
if bit == 0:
|
||||
return 0
|
||||
return int(item.get("anti_flags", 0)) & bit
|
||||
|
||||
static func _fail(code: String, need := 0) -> Dictionary:
|
||||
return {"ok": false, "code": code, "need": need}
|
||||
|
||||
|
||||
# 主校验。
|
||||
# item : proto 字典 —— {type, sub_type, anti_flags, wear_flags, limits:[{type,value}]}
|
||||
# wear : 目标 EWearPositions(或新装备槽 19..23);传 -1 用 default_wear_for(item)
|
||||
# ctx : {race, sex, job, empire, level, st, dx, ht, iq}
|
||||
# —— sex/job 缺省时由 race 推;属性 st/dx/ht/iq 缺省或 < 0 = 不判(seam)
|
||||
# 返回 {ok: bool, code: String, need: int}。code 见下方常量注释;need = 失败 limit 的要求值。
|
||||
# 主校验(顺序 1:1 = 服务器 CanEquipNow → FindEquipCell → EquipItem 后置检查)。
|
||||
# item : proto 字典 —— {type, sub_type, anti_flags, wear_flags, limits:[{type,value}], vnum?}
|
||||
# wear : 目标槽;传 -1 用 find_equip_cell(有 ctx.worn 时含 RING/UNIQUE 占用翻转)
|
||||
# ctx : {race, sex, job, empire, level, st, dx, ht, iq,
|
||||
# worn: {cell: vnum}, # 当前穿戴(RING/UNIQUE 占用判断)
|
||||
# polymorphed: bool, # 变身中(POINT_POLYMORPH / affect 220)
|
||||
# riding: bool} # 骑乘中
|
||||
# —— sex/job 缺省时由 race 推;属性 st/dx/ht/iq 缺省或 < 0 = 不判
|
||||
# 返回 {ok: bool, code: String, need: int}。need = 失败 limit 的要求值。
|
||||
static func can_equip(item: Dictionary, wear: int, ctx: Dictionary) -> Dictionary:
|
||||
if item.is_empty():
|
||||
return _fail("NO_ITEM")
|
||||
var target := wear if wear >= 0 else default_wear_for(item)
|
||||
if target < 0:
|
||||
return _fail("NOT_EQUIPPABLE")
|
||||
|
||||
var anti := int(item.get("anti_flags", 0))
|
||||
var race := int(ctx.get("race", 0))
|
||||
var sex := int(ctx.get("sex", sex_of_race(race)))
|
||||
var job := int(ctx.get("job", job_of_race(race)))
|
||||
var empire := int(ctx.get("empire", 0))
|
||||
var worn: Dictionary = ctx.get("worn", {})
|
||||
|
||||
# 1) anti-flag:性别
|
||||
if sex == SEX_FEMALE and (anti & ANTIFLAG_FEMALE):
|
||||
return _fail("ANTI_SEX")
|
||||
if sex == SEX_MALE and (anti & ANTIFLAG_MALE):
|
||||
return _fail("ANTI_SEX")
|
||||
# 2) anti-flag:职业
|
||||
# ---- CanEquipNow(char_item.cpp:7462)----
|
||||
# 1) anti-flag:职业(服务器只查 JOB;性别在拿到槽位后查,见 5))
|
||||
if job >= 0 and job < JOB_ANTIFLAG.size() and (anti & int(JOB_ANTIFLAG[job])):
|
||||
return _fail("ANTI_JOB")
|
||||
# 3) anti-flag:帝国
|
||||
var emp_bit := int(EMPIRE_ANTIFLAG.get(empire, 0))
|
||||
if emp_bit != 0 and (anti & emp_bit):
|
||||
return _fail("ANTI_EMPIRE")
|
||||
|
||||
# 4) wearable_flag 与目标槽匹配
|
||||
if not slot_accepts(item, target):
|
||||
return _fail("NOT_WEARABLE_HERE")
|
||||
|
||||
# 5) limit(等级 / 属性)—— 逐条 GetLimit
|
||||
# 2) limit(等级 / 属性)—— 逐条 GetLimit;PCBANG / REAL_TIME* 不是门禁(见文件头)
|
||||
for lim in item.get("limits", []):
|
||||
var t := int(lim.get("type", 0))
|
||||
var v := int(lim.get("value", 0))
|
||||
@@ -226,7 +287,43 @@ static func can_equip(item: Dictionary, wear: int, ctx: Dictionary) -> Dictionar
|
||||
if _stat(ctx, "ht") >= 0 and _stat(ctx, "ht") < v:
|
||||
return _fail("LIMIT_CON", v)
|
||||
_:
|
||||
pass # PCBANG / REAL_TIME* → 服务器判(seam)
|
||||
pass
|
||||
# 3) WEARABLE_UNIQUE 同组去重:UNIQUE1/UNIQUE2 已有同组物品则拒绝。
|
||||
# 服务器用 IsSameSpecialGroup;穿戴表(worn: cell→vnum)拿不到被占物品的
|
||||
# proto,这里退化为同 vnum 判定——只拦下确定的重复,其余交服务器权威。
|
||||
# 结婚戒指另需已婚,客户端不判。
|
||||
if int(item.get("wear_flags", 0)) & WEARABLE_UNIQUE and item.get("vnum", 0) is int:
|
||||
for cell in [WEAR_UNIQUE1, WEAR_UNIQUE2]:
|
||||
if int(worn.get(cell, 0)) == int(item.get("vnum", 0)) \
|
||||
and int(item.get("vnum", 0)) != 0:
|
||||
return _fail("UNIQUE_DUP")
|
||||
|
||||
# ---- FindEquipCell(item.cpp:472)----
|
||||
var target := find_equip_cell(item, worn) if wear < 0 else wear
|
||||
if target < 0:
|
||||
return _fail("NOT_EQUIPPABLE")
|
||||
# 显式指定槽时校验物品与槽匹配(服务器对显式 cell 不做这层校验,属客户端防御)。
|
||||
if wear >= 0 and not slot_accepts(item, target):
|
||||
return _fail("NOT_WEARABLE_HERE")
|
||||
|
||||
# ---- EquipItem 后置检查(char_item.cpp:6122+)----
|
||||
# 4) 燕尾服(vnum 11901..11904)骑乘中不可穿
|
||||
if target == WEAR_BODY and bool(ctx.get("riding", false)):
|
||||
var vnum := int(item.get("vnum", 0))
|
||||
if vnum >= 11901 and vnum <= 11904:
|
||||
return _fail("RIDE_TUXEDO")
|
||||
# 5) 变身中除 WEAR_ARROW 一律禁止
|
||||
if bool(ctx.get("polymorphed", false)) and target != WEAR_ARROW:
|
||||
return _fail("POLYMORPH")
|
||||
# 6) FN_check_item_sex(char_item.cpp:189)
|
||||
if sex == SEX_FEMALE and (anti & ANTIFLAG_FEMALE):
|
||||
return _fail("ANTI_SEX")
|
||||
if sex == SEX_MALE and (anti & ANTIFLAG_MALE):
|
||||
return _fail("ANTI_SEX")
|
||||
# 7) 骑乘物品已骑乘禁止(item.cpp:1695 IsRideItem)
|
||||
if bool(ctx.get("riding", false)) and is_ride_item(item):
|
||||
return _fail("RIDE_ITEM")
|
||||
# 攻击/施法后 1.5s 冷却(char_item.cpp:6143)客户端不判,交服务器。
|
||||
return {"ok": true, "code": "", "need": 0}
|
||||
|
||||
|
||||
|
||||
+67
-11
@@ -1,4 +1,6 @@
|
||||
# equip_rules_test —— §4.1 修改 2:装备穿戴合法性校验(EquipRules)1:1 自检。
|
||||
# equip_rules_test —— 装备穿戴合法性校验(EquipRules)1:1 自检。
|
||||
# 顺序对齐服务器真实链:CanEquipNow(JOB→limit→UNIQUE 去重) → FindEquipCell →
|
||||
# EquipItem 后置(燕尾服/变身/性别/骑乘)。
|
||||
# canonical #17。 godot --headless --path project --script equip_rules_test.gd
|
||||
extends SceneTree
|
||||
|
||||
@@ -24,8 +26,10 @@ func _init() -> void:
|
||||
_test_wearable()
|
||||
_test_limits()
|
||||
_test_order_and_newslots()
|
||||
_test_find_equip_cell()
|
||||
_test_post_checks()
|
||||
if failed == 0:
|
||||
print("PASS: equip_rules_test (§4.1 CanEquipNow antiflag→wearable→limit 1:1)")
|
||||
print("PASS: equip_rules_test (CanEquipNow→FindEquipCell→EquipItem 后置 1:1)")
|
||||
quit(0)
|
||||
else:
|
||||
quit(1)
|
||||
@@ -78,10 +82,13 @@ func _test_antiflag() -> void:
|
||||
_ck(EquipRules.can_equip(no_warrior, -1, {"race": 0})["code"] == "ANTI_JOB", "ANTI_WARRIOR blocks warrior (race 0)")
|
||||
_ck(EquipRules.can_equip(no_warrior, -1, {"race": 1})["ok"], "ANTI_WARRIOR ok for assassin (race 1)")
|
||||
|
||||
# 帝国 antiflag 不是装备门(服务器装备路径从不检查)——can_equip 放行,
|
||||
# 冲突位只由 empire_warning() 给 tooltip。
|
||||
var no_emp_a := _weapon({"anti_flags": EquipRules.ANTIFLAG_EMPIRE_A})
|
||||
_ck(EquipRules.can_equip(no_emp_a, -1, {"empire": 1})["code"] == "ANTI_EMPIRE", "ANTI_EMPIRE_A blocks empire 1")
|
||||
_ck(EquipRules.can_equip(no_emp_a, -1, {"empire": 2})["ok"], "ANTI_EMPIRE_A ok for empire 2")
|
||||
_ck(EquipRules.can_equip(no_emp_a, -1, {"empire": 0})["ok"], "no empire -> anti empire skipped")
|
||||
_ck(EquipRules.can_equip(no_emp_a, -1, {"empire": 1})["ok"], "ANTI_EMPIRE no longer blocks equip")
|
||||
_ck(EquipRules.empire_warning(no_emp_a, 1) == EquipRules.ANTIFLAG_EMPIRE_A, "empire_warning reports conflict")
|
||||
_ck(EquipRules.empire_warning(no_emp_a, 2) == 0, "empire_warning clean for empire 2")
|
||||
_ck(EquipRules.empire_warning(no_emp_a, 0) == 0, "empire_warning clean without empire")
|
||||
|
||||
func _test_wearable() -> void:
|
||||
var res := EquipRules.can_equip(_weapon(), EquipRules.WEAR_BODY, {"race": 0})
|
||||
@@ -109,16 +116,22 @@ func _test_limits() -> void:
|
||||
_ck(EquipRules.can_equip(zero, -1, {"race": 0, "level": 1})["ok"], "limit value 0 ignored")
|
||||
|
||||
func _test_order_and_newslots() -> void:
|
||||
# anti-flag 先于 limit:两个都失败 → 返回 ANTI_SEX
|
||||
# limit 先于性别(服务器顺序:CanEquipNow 的 limit 在 EquipItem 的
|
||||
# FN_check_item_sex 之前):两个都失败 → 返回 LIMIT_LEVEL
|
||||
var both := _weapon({"anti_flags": EquipRules.ANTIFLAG_FEMALE,
|
||||
"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||||
_ck(EquipRules.can_equip(both, -1, {"race": 1, "level": 1})["code"] == "ANTI_SEX",
|
||||
"antiflag checked before limit")
|
||||
_ck(EquipRules.can_equip(both, -1, {"race": 1, "level": 1})["code"] == "LIMIT_LEVEL",
|
||||
"limit checked before sex antiflag (server order)")
|
||||
# JOB 仍是最先的 antiflag 检查
|
||||
var job_first := _weapon({"anti_flags": EquipRules.ANTIFLAG_WARRIOR,
|
||||
"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||||
_ck(EquipRules.can_equip(job_first, -1, {"race": 0, "level": 1})["code"] == "ANTI_JOB",
|
||||
"job antiflag checked before limit")
|
||||
|
||||
# wearable 先于 limit
|
||||
# 显式槽不匹配(客户端防御层)在 limit 之后
|
||||
var wl := _weapon({"limits": [{"type": EquipRules.LIMIT_LEVEL, "value": 99}]})
|
||||
_ck(EquipRules.can_equip(wl, EquipRules.WEAR_BODY, {"race": 0, "level": 1})["code"] == "NOT_WEARABLE_HERE",
|
||||
"wearable checked before limit")
|
||||
_ck(EquipRules.can_equip(wl, EquipRules.WEAR_BODY, {"race": 0, "level": 99})["code"] == "NOT_WEARABLE_HERE",
|
||||
"explicit slot mismatch -> NOT_WEARABLE_HERE")
|
||||
|
||||
# 新装备槽:ring / belt / costume 按 type 判定,无 wear_flags 也能穿
|
||||
_ck(EquipRules.can_equip({"type": 33}, EquipRules.WEAR_RING1, {"race": 0})["ok"], "ring at RING1 ok")
|
||||
@@ -129,3 +142,46 @@ func _test_order_and_newslots() -> void:
|
||||
|
||||
# 空物品
|
||||
_ck(EquipRules.can_equip({}, -1, {})["code"] == "NO_ITEM", "empty item -> NO_ITEM")
|
||||
|
||||
func _test_find_equip_cell() -> void:
|
||||
# item.cpp:472 的 else-if 优先级与 RING/UNIQUE 占用翻转。
|
||||
_ck(EquipRules.find_equip_cell(_weapon()) == EquipRules.WEAR_WEAPON, "weapon cell")
|
||||
_ck(EquipRules.find_equip_cell(_weapon({"wear_flags": (1 << 0) | (1 << 4)})) == EquipRules.WEAR_BODY,
|
||||
"BODY wins over WEAPON in else-if chain")
|
||||
_ck(EquipRules.find_equip_cell({"type": 33}) == EquipRules.WEAR_RING1, "ring -> RING1 when free")
|
||||
_ck(EquipRules.find_equip_cell({"type": 33}, {EquipRules.WEAR_RING1: 1234}) == EquipRules.WEAR_RING2,
|
||||
"ring -> RING2 when RING1 occupied")
|
||||
var uniq := {"type": 2, "wear_flags": 1 << 7, "vnum": 70001}
|
||||
_ck(EquipRules.find_equip_cell(uniq) == EquipRules.WEAR_UNIQUE1, "unique -> UNIQUE1 when free")
|
||||
_ck(EquipRules.find_equip_cell(uniq, {EquipRules.WEAR_UNIQUE1: 70002}) == EquipRules.WEAR_UNIQUE2,
|
||||
"unique -> UNIQUE2 when UNIQUE1 occupied")
|
||||
# 无 wear_flag 且非 COSTUME/DS/RING/BELT → -1(TOTEM 同)
|
||||
_ck(EquipRules.find_equip_cell({"type": 3}) == -1, "no wear flag -> -1")
|
||||
_ck(EquipRules.find_equip_cell({"type": 26, "wear_flags": 1 << 0}) == -1, "totem -> -1")
|
||||
# 龙魂石走客户端专用路径
|
||||
_ck(EquipRules.find_equip_cell({"type": 29, "sub_type": 3}) == -1, "DS -> -1 (dedicated path)")
|
||||
|
||||
func _test_post_checks() -> void:
|
||||
# EquipItem 后置门(char_item.cpp:6122+)。
|
||||
# 变身中除 ARROW 外一律拒绝
|
||||
var bow := _weapon({"wear_flags": 1 << 9})
|
||||
_ck(EquipRules.can_equip(_weapon(), -1, {"race": 0, "polymorphed": true})["code"] == "POLYMORPH",
|
||||
"polymorphed blocks weapon")
|
||||
_ck(EquipRules.can_equip(bow, -1, {"race": 0, "polymorphed": true})["ok"],
|
||||
"polymorphed allows ARROW")
|
||||
# 燕尾服骑乘禁止(vnum 11901..11904)
|
||||
var tux := {"type": 2, "sub_type": 0, "wear_flags": 1 << 0, "vnum": 11901}
|
||||
_ck(EquipRules.can_equip(tux, -1, {"race": 0, "riding": true})["code"] == "RIDE_TUXEDO",
|
||||
"tuxedo blocked while riding")
|
||||
_ck(EquipRules.can_equip(tux, -1, {"race": 0, "riding": false})["ok"], "tuxedo ok on foot")
|
||||
# 骑乘物品已骑乘禁止(UNIQUE + RIDE/MOUNT_RIDE 子类)
|
||||
var horse := {"type": 16, "sub_type": 2, "wear_flags": 1 << 7, "vnum": 50051}
|
||||
_ck(EquipRules.can_equip(horse, -1, {"race": 0, "riding": true})["code"] == "RIDE_ITEM",
|
||||
"ride item blocked while riding")
|
||||
_ck(EquipRules.can_equip(horse, -1, {"race": 0, "riding": false})["ok"], "ride item ok on foot")
|
||||
# UNIQUE 同 vnum 去重
|
||||
var u := {"type": 2, "wear_flags": 1 << 7, "vnum": 70001}
|
||||
_ck(EquipRules.can_equip(u, -1, {"race": 0, "worn": {EquipRules.WEAR_UNIQUE1: 70001}})["code"] == "UNIQUE_DUP",
|
||||
"same vnum in UNIQUE1 -> UNIQUE_DUP")
|
||||
_ck(EquipRules.can_equip(u, -1, {"race": 0, "worn": {EquipRules.WEAR_UNIQUE1: 70002}})["ok"],
|
||||
"different vnum in UNIQUE1 ok")
|
||||
|
||||
@@ -311,7 +311,14 @@ func snap_to_target() -> void:
|
||||
global_position = _desired_pos(true)
|
||||
else:
|
||||
global_position = _desired_pos(true)
|
||||
look_at(_look_target(), Vector3.UP)
|
||||
_look_at_safe()
|
||||
|
||||
# 相机位置与注视点重合时(首帧、传送落点、极近距离)方向向量为零,look_at() 会
|
||||
# 报错并且不改变朝向。跳过这一帧,下一帧插值分开后自然恢复。
|
||||
func _look_at_safe() -> void:
|
||||
var look_to := _look_target()
|
||||
if not global_position.is_equal_approx(look_to):
|
||||
look_at(look_to, Vector3.UP)
|
||||
|
||||
var _shake := 0.0 # 当前抖动强度(米),指数衰减
|
||||
var _shake_decay := 8.0
|
||||
@@ -350,7 +357,7 @@ func _process(dt: float) -> void:
|
||||
_shake = lerpf(_shake, 0.0, clampf(_shake_decay * dt, 0.0, 1.0))
|
||||
else:
|
||||
_shake = 0.0
|
||||
look_at(_look_target(), Vector3.UP)
|
||||
_look_at_safe()
|
||||
if camera_mode == CameraMode.NORMAL:
|
||||
_fade_occluders()
|
||||
|
||||
|
||||
+32
-13
@@ -182,19 +182,8 @@ func setup(m2client: Node, assets_root: String,
|
||||
if net_world.has_method("set_camera"):
|
||||
net_world.set_camera(cam)
|
||||
net_world.set_local_node(player)
|
||||
net_world.entity_added.connect(func(node: Node3D, _vid: int):
|
||||
# Model-factory upgrades replace an earlier placeholder node in place.
|
||||
# Drop queued-for-free entries before registering the rebuilt pick target.
|
||||
for old in pc.pickables.duplicate():
|
||||
if old == null or not is_instance_valid(old):
|
||||
pc.pickables.erase(old)
|
||||
if node not in pc.pickables:
|
||||
pc.pickables.append(node))
|
||||
net_world.entity_removed.connect(func(vid: int):
|
||||
for pickable in pc.pickables.duplicate():
|
||||
if pickable == null or not is_instance_valid(pickable) \
|
||||
or int(pickable.get_meta("vid", -1)) == vid:
|
||||
pc.pickables.erase(pickable))
|
||||
net_world.entity_added.connect(_on_pickable_added)
|
||||
net_world.entity_removed.connect(_on_pickable_removed)
|
||||
|
||||
if _ui_mobile:
|
||||
hud = MobileUiRoot.new()
|
||||
@@ -611,6 +600,36 @@ func _setup_mobile_ui() -> void:
|
||||
})
|
||||
set_process(true)
|
||||
|
||||
# 可点选实体列表维护。pickables 是 Array[Node3D](TypedArray),对已释放的实例
|
||||
# 调用 erase()/has() 会被类型校验拒绝并报错,所以一律按下标倒序 remove_at()。
|
||||
func _drop_stale_pickables(drop_vid: int = -1) -> void:
|
||||
if pc == null:
|
||||
return
|
||||
var list: Array = pc.pickables
|
||||
for i in range(list.size() - 1, -1, -1):
|
||||
# 不要把元素赋给 Node3D 类型的变量:已释放的实例会在类型校验时再次报错。
|
||||
var n = list[i]
|
||||
if n == null or not is_instance_valid(n):
|
||||
list.remove_at(i)
|
||||
elif n.is_queued_for_deletion():
|
||||
list.remove_at(i)
|
||||
elif drop_vid >= 0 and int(n.get_meta("vid", -1)) == drop_vid:
|
||||
list.remove_at(i)
|
||||
|
||||
# 模型工厂升级会原地替换先前的占位节点,注册新的点选目标前先清掉待删条目。
|
||||
func _on_pickable_added(node: Node3D, _vid: int) -> void:
|
||||
if pc == null or node == null:
|
||||
return
|
||||
_drop_stale_pickables()
|
||||
var list: Array = pc.pickables
|
||||
for existing in list:
|
||||
if existing == node:
|
||||
return
|
||||
list.append(node)
|
||||
|
||||
func _on_pickable_removed(vid: int) -> void:
|
||||
_drop_stale_pickables(vid)
|
||||
|
||||
func _lifecycle_node() -> Node:
|
||||
# AppFlow owns the single lifecycle observer across login/select/game. The
|
||||
# game scene only borrows it for Android back and focus recovery.
|
||||
|
||||
@@ -17,6 +17,9 @@ class FakeClient extends Node:
|
||||
func get_guild() -> Dictionary: return guild
|
||||
func get_guild_members() -> Array: return members
|
||||
func get_guild_grades() -> Array: return grades
|
||||
func get_main_pid() -> int: return 2 # Newbie(grade 3,auth 0 → 无踢人权)
|
||||
func guild_deposit_money(v: int) -> bool: calls.append(["deposit", v]); return true
|
||||
func guild_withdraw_money(v: int) -> bool: calls.append(["withdraw", v]); return true
|
||||
func refine(pos, type) -> bool: calls.append(["refine", pos, type]); return true
|
||||
|
||||
var _fail := 0
|
||||
@@ -56,10 +59,17 @@ func _run() -> void:
|
||||
]
|
||||
fc.guild_changed.emit()
|
||||
_ck(gu._info.text.contains("Dragons") and gu._info.text.contains("Lv.12"), "公会信息行")
|
||||
_ck(gu._pages[0].get_child_count() == 2, "2 个成员行")
|
||||
# 成员页 = 1 资金行 + 2 成员行(成员行是 HBox:Label [+ 踢出按钮])
|
||||
var rows: Array = gu._pages[0].get_children()
|
||||
_ck(rows.size() == 3, "资金行 + 2 个成员行")
|
||||
_ck(String((rows[0].get_child(0) as Label).text).contains("500000"), "资金行显示公会资金")
|
||||
var member_labels: Array[String] = []
|
||||
for r in rows.slice(1):
|
||||
member_labels.append(String((r.get_child(0) as Label).text))
|
||||
_ck(r.get_child_count() == 1, "无 REMOVE_MEMBER 权限 -> 无踢出按钮")
|
||||
# 会长排前(grade 1 < grade 3)
|
||||
_ck(String((gu._pages[0].get_child(0) as Label).text).begins_with("Master"), "按 grade 排序")
|
||||
_ck(String((gu._pages[0].get_child(0) as Label).text).contains("[会长]"), "grade 名解析")
|
||||
_ck(member_labels[0].begins_with("Master"), "按 grade 排序")
|
||||
_ck(member_labels[0].contains("[会长]"), "grade 名解析")
|
||||
|
||||
# --- 精炼对话框 ---
|
||||
var ru: Node = RefineUI.new()
|
||||
|
||||
+17
-2
@@ -32,13 +32,28 @@ func _run() -> void:
|
||||
get_root().add_child(love)
|
||||
love.setup(client, canvas)
|
||||
_check(not love._root.visible, "no lover keeps panel hidden")
|
||||
client.lover = {"valid": true, "name": "Partner", "love_point": 63}
|
||||
client.lover = {"valid": true, "name": "Partner", "love_point": 63,
|
||||
"online": false, "near": false}
|
||||
client.lover_changed.emit(client.lover)
|
||||
await process_frame
|
||||
_check(love._root.visible, "lover info shows panel")
|
||||
_check(love._name.text == "♥ Partner", "lover name rendered")
|
||||
_check(love._name.text == "♥ Partner(离线)", "lover name + offline state rendered")
|
||||
_check(int(love._bar.value) == 63, "lover point rendered")
|
||||
client.lover["love_point"] = 88
|
||||
client.lover_changed.emit(client.lover)
|
||||
await process_frame
|
||||
_check(int(love._bar.value) == 88, "love point update refreshes bar")
|
||||
# lover_login / lover_near(CHAT_TYPE_COMMAND,marriage.cpp)→ 状态后缀。
|
||||
client.lover["online"] = true
|
||||
client.lover_changed.emit(client.lover)
|
||||
await process_frame
|
||||
_check(love._name.text == "♥ Partner(在线)", "lover_online state rendered")
|
||||
client.lover["near"] = true
|
||||
client.lover_changed.emit(client.lover)
|
||||
await process_frame
|
||||
_check(love._name.text == "♥ Partner(在身边)", "lover_near state rendered")
|
||||
# lover_divorce → store 清空 LoverInfo → 面板隐藏。
|
||||
client.lover = {"valid": false, "name": "", "love_point": 0}
|
||||
client.lover_changed.emit(client.lover)
|
||||
await process_frame
|
||||
_check(not love._root.visible, "lover divorce hides panel")
|
||||
|
||||
@@ -40,6 +40,15 @@ var _accept_pending := false
|
||||
var _anchor := Vector2.ZERO
|
||||
var _anchor_valid := false
|
||||
|
||||
# EXCHANGE_GC_ALREADY / LESS_GOLD 的线上的子头值(服务器 exchange.cpp)。
|
||||
# GC_END 在成功与取消两种结局都会发,成败文案只走 CHAT_TYPE_INFO 聊天通道。
|
||||
const NOTICE_ALREADY := 6
|
||||
const NOTICE_LESS_GOLD := 7
|
||||
var _was_active := false
|
||||
var _notice_shown := false
|
||||
var _end_note: Label
|
||||
var _end_note_timer: Timer
|
||||
|
||||
func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
client = m2client
|
||||
proto = proto_node
|
||||
@@ -123,6 +132,11 @@ func refresh() -> void:
|
||||
return
|
||||
var x: Dictionary = client.get_exchange()
|
||||
if not x.get("active", false):
|
||||
if _was_active:
|
||||
# 窗口开着时服务器关掉了交易(成功或取消,二者同发 GC_END)。
|
||||
_show_end_note(int(x.get("notice", 0)))
|
||||
_was_active = false
|
||||
_notice_shown = false
|
||||
_root.visible = false
|
||||
_next_display = 0
|
||||
_offered_cells.clear()
|
||||
@@ -131,6 +145,7 @@ func refresh() -> void:
|
||||
_gold_input.text = ""
|
||||
_status.text = ""
|
||||
return
|
||||
_was_active = true
|
||||
_root.visible = true
|
||||
_capture_anchor()
|
||||
_render_self_grid(x.get("self_items", []))
|
||||
@@ -147,9 +162,43 @@ func refresh() -> void:
|
||||
_accept_btn.disabled = me
|
||||
_accept_btn.modulate = Color(0.5, 1, 0.5) if me else Color(1, 1, 1)
|
||||
_root.get_node("PeerAccept").text = "对方: 已接受" if peer else "对方: 未接受"
|
||||
_status.text = ""
|
||||
var notice := int(x.get("notice", 0))
|
||||
if notice != 0 and not _notice_shown:
|
||||
_notice_shown = true
|
||||
_status.text = _notice_text(notice)
|
||||
else:
|
||||
_status.text = ""
|
||||
_refresh_mobile_inventory()
|
||||
|
||||
func _notice_text(code: int) -> String:
|
||||
match code:
|
||||
NOTICE_ALREADY:
|
||||
return "对方已经在交易中"
|
||||
NOTICE_LESS_GOLD:
|
||||
return "金币不足"
|
||||
return ""
|
||||
|
||||
# 交易被服务器关闭时的一条短暂浮动提示(成败详情以 CHAT_TYPE_INFO 聊天为准)。
|
||||
func _show_end_note(code: int) -> void:
|
||||
if _root == null or _root.get_parent() == null:
|
||||
return
|
||||
if _end_note == null:
|
||||
_end_note = Label.new()
|
||||
_end_note.add_theme_font_size_override("font_size", 15)
|
||||
_end_note.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||||
_end_note.position = Vector2(-110, 130)
|
||||
_end_note.size = Vector2(220, 24)
|
||||
_end_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_root.get_parent().add_child(_end_note)
|
||||
_end_note_timer = Timer.new()
|
||||
_end_note_timer.one_shot = true
|
||||
_end_note_timer.timeout.connect(func(): if _end_note: _end_note.visible = false)
|
||||
add_child(_end_note_timer)
|
||||
var extra := _notice_text(code)
|
||||
_end_note.text = ("交易结束(%s)" % extra) if extra != "" else "交易结束,结果见聊天提示"
|
||||
_end_note.visible = true
|
||||
_end_note_timer.start(5.0)
|
||||
|
||||
func _apply_mobile_layout() -> void:
|
||||
if _root == null:
|
||||
return
|
||||
|
||||
+92
-4
@@ -29,6 +29,11 @@ func setup(m2client: Node, parent: Node) -> void:
|
||||
client.friends_changed.connect(refresh)
|
||||
if client.has_signal("friend_invite_ask"):
|
||||
client.friend_invite_ask.connect(_on_friend_invite)
|
||||
# 原版 messenger 是三固定分组(uimessenger.py MessengerFriendGroup/GuildGroup/
|
||||
# FamilyGroup);公会与情侣组数据变化时同样刷新。
|
||||
for sig in ["guild_changed", "lover_changed"]:
|
||||
if client.has_signal(sig):
|
||||
client.connect(sig, refresh)
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
@@ -58,15 +63,98 @@ func refresh() -> void:
|
||||
return
|
||||
for c in _list.get_children():
|
||||
c.queue_free()
|
||||
var friends: Array = client.get_friends()
|
||||
# 三固定分组(对齐原版 MessengerWindow):好友 / 公会 / 情侣。
|
||||
_section("好友")
|
||||
var friends: Array = client.get_friends() if client.has_method("get_friends") else []
|
||||
if friends.is_empty():
|
||||
var e := Label.new()
|
||||
e.text = "(好友列表为空)"
|
||||
e.add_theme_font_size_override("font_size", 11)
|
||||
e.modulate = Color(0.6, 0.6, 0.6)
|
||||
_list.add_child(e)
|
||||
return
|
||||
for f in friends:
|
||||
_list.add_child(_row(f))
|
||||
else:
|
||||
for f in friends:
|
||||
_list.add_child(_row(f))
|
||||
_section("公会")
|
||||
if client.has_method("get_guild") and client.get_guild().get("in_guild", false) \
|
||||
and client.has_method("get_guild_members"):
|
||||
var members: Array = client.get_guild_members()
|
||||
members.sort_custom(func(a, b):
|
||||
return int(a.get("grade", 99)) < int(b.get("grade", 99)))
|
||||
for m in members:
|
||||
_list.add_child(_guild_row(m))
|
||||
else:
|
||||
var g := Label.new()
|
||||
g.text = "(未加入公会)"
|
||||
g.add_theme_font_size_override("font_size", 11)
|
||||
g.modulate = Color(0.6, 0.6, 0.6)
|
||||
_list.add_child(g)
|
||||
_section("情侣")
|
||||
if client.has_method("get_lover") and client.get_lover().get("valid", false):
|
||||
_list.add_child(_lover_row(client.get_lover()))
|
||||
else:
|
||||
var l := Label.new()
|
||||
l.text = "(暂无)"
|
||||
l.add_theme_font_size_override("font_size", 11)
|
||||
l.modulate = Color(0.6, 0.6, 0.6)
|
||||
_list.add_child(l)
|
||||
|
||||
func _section(title: String) -> void:
|
||||
var h := Label.new()
|
||||
h.text = "— %s —" % title
|
||||
h.add_theme_font_size_override("font_size", 12)
|
||||
h.modulate = Color(0.75, 0.82, 0.95)
|
||||
_list.add_child(h)
|
||||
|
||||
# 公会组成员:在线态来自 GC_GUILD_LOGIN/LOGOUT;点按同样发起私聊。
|
||||
func _guild_row(m: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(640, 46) if _mobile_mode else Vector2(260, 0)
|
||||
var dot := Label.new()
|
||||
dot.text = "●"
|
||||
dot.custom_minimum_size = Vector2(28, 42) if _mobile_mode else Vector2.ZERO
|
||||
dot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
dot.modulate = Color(0.4, 0.9, 0.4) if m.get("online", false) else Color(0.4, 0.4, 0.4)
|
||||
row.add_child(dot)
|
||||
var nm := Button.new()
|
||||
nm.text = String(m.get("name", "?"))
|
||||
nm.flat = true
|
||||
nm.custom_minimum_size = Vector2(450, 42) if _mobile_mode else Vector2(150, 0)
|
||||
nm.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
nm.pressed.connect(func() -> void: whisper_to.emit(String(m.get("name", ""))))
|
||||
row.add_child(nm)
|
||||
if int(m.get("grade", 1)) == 0:
|
||||
var lead := Label.new()
|
||||
lead.text = "会长"
|
||||
lead.modulate = Color(0.95, 0.8, 0.4)
|
||||
lead.add_theme_font_size_override("font_size", 10)
|
||||
row.add_child(lead)
|
||||
return row
|
||||
|
||||
# 情侣组成员:lover_login/logout/near/far(CHAT_TYPE_COMMAND,marriage.cpp)。
|
||||
func _lover_row(lover: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(640, 46) if _mobile_mode else Vector2(260, 0)
|
||||
var dot := Label.new()
|
||||
dot.text = "♥"
|
||||
dot.custom_minimum_size = Vector2(28, 42) if _mobile_mode else Vector2.ZERO
|
||||
dot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
dot.modulate = Color(0.95, 0.4, 0.55) if lover.get("online", false) else Color(0.4, 0.4, 0.4)
|
||||
row.add_child(dot)
|
||||
var nm := Button.new()
|
||||
nm.text = String(lover.get("name", "?"))
|
||||
nm.flat = true
|
||||
nm.custom_minimum_size = Vector2(450, 42) if _mobile_mode else Vector2(150, 0)
|
||||
nm.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
nm.pressed.connect(func() -> void: whisper_to.emit(String(lover.get("name", ""))))
|
||||
row.add_child(nm)
|
||||
if lover.get("near", false):
|
||||
var near := Label.new()
|
||||
near.text = "在身边"
|
||||
near.modulate = Color(1.0, 0.6, 0.7)
|
||||
near.add_theme_font_size_override("font_size", 10)
|
||||
row.add_child(near)
|
||||
return row
|
||||
|
||||
func _row(f: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
|
||||
+159
-4
@@ -34,6 +34,13 @@ var _create_dialog: ConfirmationDialog
|
||||
var _create_name_edit: LineEdit
|
||||
var _create_status: Label
|
||||
var _mobile_mode := false
|
||||
var _war_ask_dialog: ConfirmationDialog
|
||||
|
||||
# GUILD_AUTH_*(服务器 guild.h:94-97):等级权限位。会长(grade 0)恒全权。
|
||||
const AUTH_ADD_MEMBER := 1 << 0
|
||||
const AUTH_REMOVE_MEMBER := 1 << 1
|
||||
const AUTH_NOTICE := 1 << 2
|
||||
const AUTH_USE_SKILL := 1 << 3
|
||||
|
||||
const MOBILE_SIZE := Vector2(700, 340)
|
||||
|
||||
@@ -51,6 +58,10 @@ func setup(m2client: Node, parent: Node, table: Object = null) -> void:
|
||||
client.guild_marks_ready.connect(func(_n): refresh())
|
||||
if client.has_signal("guild_mark_uploaded"):
|
||||
client.guild_mark_uploaded.connect(_on_mark_uploaded)
|
||||
# GUILD_WAR_RECV_DECLARE(3) 时弹被宣战应答框(原版 AcceptGuildWarDialog:
|
||||
# 接受发 /war <name>,拒绝发 /nowar <name>,均走 chat 通道)。
|
||||
if client.has_signal("guild_war_event"):
|
||||
client.guild_war_event.connect(_on_guild_war_event)
|
||||
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
@@ -102,6 +113,70 @@ func refresh() -> void:
|
||||
2: _fill_war(int(g.get("id", 0)))
|
||||
3: _fill_comments()
|
||||
|
||||
# 我的等级权限位(会长 grade 0 恒 0xF;否则取 grades[grade-1].auth)。
|
||||
# 成员/等级数据未同步(成员表为空、找不到自己)时**返回全权**——权限只是 UI 预判,
|
||||
# 不可判定时不拦操作,由服务器权威兜底(与原版一致:原版也不在前端做这层门)。
|
||||
const AUTH_ALL := AUTH_ADD_MEMBER | AUTH_REMOVE_MEMBER | AUTH_NOTICE | AUTH_USE_SKILL
|
||||
|
||||
func _my_auth() -> int:
|
||||
if client == null or not client.has_method("get_guild_grades") \
|
||||
or not client.has_method("get_main_pid") \
|
||||
or not client.has_method("get_guild_members"):
|
||||
return AUTH_ALL
|
||||
var my_pid := int(client.get_main_pid())
|
||||
var my_grade := -1
|
||||
for m in client.get_guild_members():
|
||||
if int(m.get("pid", 0)) == my_pid:
|
||||
my_grade = int(m.get("grade", -1))
|
||||
break
|
||||
if my_grade == -1:
|
||||
return AUTH_ALL # 成员表里还没有自己(未同步)
|
||||
if my_grade == 0:
|
||||
return AUTH_ALL # GUILD_LEADER_GRADE
|
||||
var grades: Array = client.get_guild_grades()
|
||||
if my_grade >= 1 and my_grade <= grades.size():
|
||||
return int(grades[my_grade - 1].get("auth", 0))
|
||||
return AUTH_ALL
|
||||
|
||||
# 被宣战应答(对齐原版 __GuildWar_OpenAskDialog / OnAccept / OnDecline)。
|
||||
func _on_guild_war_event(state: int, opp_guild_id: int, opp_name: String) -> void:
|
||||
if state != 3 or opp_guild_id == 0: # GUILD_WAR_RECV_DECLARE
|
||||
return
|
||||
if is_instance_valid(_war_ask_dialog):
|
||||
_war_ask_dialog.queue_free()
|
||||
_war_ask_dialog = ConfirmationDialog.new()
|
||||
_war_ask_dialog.title = "公会战邀请"
|
||||
var shown := opp_name if opp_name != "" else "#%d" % opp_guild_id
|
||||
_war_ask_dialog.dialog_text = "公会 [%s] 向我方宣战,接受吗?" % shown
|
||||
_war_ask_dialog.ok_button_text = "接受"
|
||||
_war_ask_dialog.cancel_button_text = "拒绝"
|
||||
_war_ask_dialog.confirmed.connect(func() -> void:
|
||||
if client.has_method("declare_guild_war"):
|
||||
client.declare_guild_war(opp_name) # /war <name>
|
||||
_war_ask_dialog.queue_free()
|
||||
_war_ask_dialog = null)
|
||||
_war_ask_dialog.canceled.connect(func() -> void:
|
||||
if client.has_method("say") and opp_name != "":
|
||||
client.say(0, "/nowar " + opp_name) # 原版 __GuildWar_OnDecline
|
||||
_war_ask_dialog.queue_free()
|
||||
_war_ask_dialog = null)
|
||||
if _root and _root.get_parent():
|
||||
_root.get_parent().add_child(_war_ask_dialog)
|
||||
if _mobile_mode:
|
||||
_war_ask_dialog.set_meta("mobile_modal", true)
|
||||
_war_ask_dialog.set_meta("mobile_title", "公会战邀请")
|
||||
_war_ask_dialog.popup_centered()
|
||||
|
||||
func _my_grade_is_leader() -> bool:
|
||||
if client == null or not client.has_method("get_main_pid") \
|
||||
or not client.has_method("get_guild_members"):
|
||||
return false
|
||||
var my_pid := int(client.get_main_pid())
|
||||
for m in client.get_guild_members():
|
||||
if int(m.get("pid", 0)) == my_pid:
|
||||
return int(m.get("grade", -1)) == 0 # GUILD_LEADER_GRADE
|
||||
return false
|
||||
|
||||
func _fill_members() -> void:
|
||||
var page := _pages[0]
|
||||
for c in page.get_children():
|
||||
@@ -125,14 +200,58 @@ func _fill_members() -> void:
|
||||
|
||||
var grades: Array = client.get_guild_grades()
|
||||
var members: Array = client.get_guild_members()
|
||||
|
||||
# 公会资金存取(GUILD_CG_DEPOSIT/WITHDRAW_MONEY = 13/14)。40250 服务器把
|
||||
# 两个处理分支都禁用了(input_main.cpp 直接 return),但原版 uiguild.py 的
|
||||
# OnDeposit/OnWithdraw 仍照发——保持 1:1,结果以服务器回执为准。
|
||||
if client.has_method("guild_deposit_money"):
|
||||
var money := HBoxContainer.new()
|
||||
money.add_theme_constant_override("separation", 6)
|
||||
page.add_child(money)
|
||||
var ginfo: Dictionary = client.get_guild()
|
||||
var mlabel := Label.new()
|
||||
mlabel.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
|
||||
mlabel.custom_minimum_size = Vector2(300, 40) if _mobile_mode else Vector2(160, 0)
|
||||
mlabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
mlabel.text = "公会资金: %d" % int(ginfo.get("gold", 0))
|
||||
money.add_child(mlabel)
|
||||
var amount := LineEdit.new()
|
||||
amount.placeholder_text = "金额"
|
||||
amount.max_length = 9
|
||||
amount.custom_minimum_size = Vector2(160, 40) if _mobile_mode else Vector2(96, 26)
|
||||
money.add_child(amount)
|
||||
var dep := Button.new()
|
||||
dep.text = "存入"
|
||||
dep.tooltip_text = "40250 服务器已禁用公会存款"
|
||||
dep.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 26)
|
||||
dep.pressed.connect(func():
|
||||
var v := int(amount.text)
|
||||
if v > 0:
|
||||
client.guild_deposit_money(v))
|
||||
money.add_child(dep)
|
||||
var wdr := Button.new()
|
||||
wdr.text = "取出"
|
||||
wdr.tooltip_text = "40250 服务器已禁用公会取款"
|
||||
wdr.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 26)
|
||||
wdr.pressed.connect(func():
|
||||
var v := int(amount.text)
|
||||
if v > 0:
|
||||
client.guild_withdraw_money(v))
|
||||
money.add_child(wdr)
|
||||
|
||||
var can_kick := (_my_auth() & AUTH_REMOVE_MEMBER) != 0
|
||||
var my_pid := int(client.get_main_pid()) if client.has_method("get_main_pid") else 0
|
||||
members.sort_custom(func(a, b): return int(a.get("grade", 99)) < int(b.get("grade", 99)))
|
||||
for m in members:
|
||||
var gi := int(m.get("grade", 0))
|
||||
var gname := ""
|
||||
if gi >= 1 and gi <= grades.size():
|
||||
gname = String(grades[gi - 1].get("name", ""))
|
||||
var mrow := HBoxContainer.new()
|
||||
mrow.add_theme_constant_override("separation", 6)
|
||||
page.add_child(mrow)
|
||||
var row := Label.new()
|
||||
row.custom_minimum_size = Vector2(640, 34) if _mobile_mode else Vector2.ZERO
|
||||
row.custom_minimum_size = Vector2(560, 34) if _mobile_mode else Vector2.ZERO
|
||||
row.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
row.add_theme_font_size_override("font_size", 14 if _mobile_mode else 12)
|
||||
row.text = "%s Lv.%d [%s] 贡献 %d%s" % [
|
||||
@@ -140,7 +259,17 @@ func _fill_members() -> void:
|
||||
gname if gname != "" else str(gi),
|
||||
int(m.get("offer", 0)),
|
||||
(" 在线" if m.get("online", false) else " 离线") + (" ★" if m.get("general", false) else "")]
|
||||
page.add_child(row)
|
||||
mrow.add_child(row)
|
||||
# 成员管理:REMOVE_MEMBER 权限位可见的踢出按钮(自己与会长不可踢,服务器兜底)。
|
||||
var pid := int(m.get("pid", 0))
|
||||
if can_kick and pid != 0 and pid != my_pid and gi != 0:
|
||||
var kick := Button.new()
|
||||
kick.text = "踢出"
|
||||
kick.custom_minimum_size = Vector2(72, 34) if _mobile_mode else Vector2(44, 24)
|
||||
kick.pressed.connect(func():
|
||||
if client.has_method("guild_remove_member"):
|
||||
client.guild_remove_member(pid))
|
||||
mrow.add_child(kick)
|
||||
|
||||
func _fill_comments() -> void:
|
||||
var page := _pages[3]
|
||||
@@ -154,6 +283,10 @@ func _fill_comments() -> void:
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 5)
|
||||
page.add_child(actions)
|
||||
var board_status := Label.new()
|
||||
board_status.add_theme_font_size_override("font_size", 11)
|
||||
board_status.add_theme_color_override("font_color", Color(1, 0.65, 0.55))
|
||||
page.add_child(board_status)
|
||||
var edit := LineEdit.new()
|
||||
edit.placeholder_text = "公告内容(最多 50 字)"
|
||||
edit.custom_minimum_size = Vector2(440, 40) if _mobile_mode else Vector2(245, 26)
|
||||
@@ -163,7 +296,14 @@ func _fill_comments() -> void:
|
||||
post.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(52, 26)
|
||||
post.pressed.connect(func():
|
||||
var text := edit.text.strip_edges()
|
||||
if text != "" and client.has_method("guild_post_comment"):
|
||||
if text == "":
|
||||
return
|
||||
# 服务器 input_main.cpp:2819:'!' 前缀(公告)需要 GUILD_AUTH_NOTICE。
|
||||
if text.begins_with("!") and (_my_auth() & AUTH_NOTICE) == 0:
|
||||
board_status.text = "发布公告需要相应权限"
|
||||
return
|
||||
board_status.text = ""
|
||||
if client.has_method("guild_post_comment"):
|
||||
client.guild_post_comment(text))
|
||||
actions.add_child(post)
|
||||
var refresh_btn := Button.new()
|
||||
@@ -226,6 +366,11 @@ func _fill_skills() -> void:
|
||||
page.add_child(head)
|
||||
var levels: Array = sk.get("levels", [])
|
||||
var meta := _guild_skill_rows()
|
||||
var auth := _my_auth()
|
||||
var can_use_skill := (auth & AUTH_USE_SKILL) != 0
|
||||
# do_guildskillup 只认会长(GUILD_LEADER_GRADE);技能点 > 0 才显示升级按钮。
|
||||
var is_leader := auth == AUTH_ALL and _my_grade_is_leader()
|
||||
var has_point := int(sk.get("skill_point", 0)) > 0
|
||||
for i in levels.size():
|
||||
var lv := int(levels[i])
|
||||
var nm := "公会技能 %d" % i
|
||||
@@ -242,12 +387,22 @@ func _fill_skills() -> void:
|
||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
lbl.text = "%s Lv %d" % [nm, lv]
|
||||
row.add_child(lbl)
|
||||
if sid >= 0 and lv > 0:
|
||||
# 施放:服务器 guild.cpp:1248 要求 GUILD_AUTH_USE_SKILL。
|
||||
if sid >= 0 and lv > 0 and can_use_skill:
|
||||
var btn := Button.new()
|
||||
btn.text = "施放"
|
||||
btn.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 24)
|
||||
btn.pressed.connect(func(): client.use_guild_skill(sid, 0))
|
||||
row.add_child(btn)
|
||||
# 升级:/gskillup <技能 vnum>(cmd_general.cpp do_guildskillup,仅会长)。
|
||||
if sid >= 0 and is_leader and has_point:
|
||||
var up := Button.new()
|
||||
up.text = "升级"
|
||||
up.custom_minimum_size = Vector2(92, 40) if _mobile_mode else Vector2(56, 24)
|
||||
up.pressed.connect(func():
|
||||
if client.has_method("say"):
|
||||
client.say(0, "/gskillup %d" % sid))
|
||||
row.add_child(up)
|
||||
|
||||
func _fill_war(my_guild_id: int) -> void:
|
||||
var page := _pages[2]
|
||||
|
||||
@@ -666,14 +666,47 @@ func can_equip_item(vnum: int, wear := -1) -> Dictionary:
|
||||
return {"ok": false, "code": "NO_ITEM", "need": 0}
|
||||
return EquipRules.can_equip(proto.item(vnum), wear, _equip_ctx())
|
||||
|
||||
# 组装 EquipRules.can_equip 需要的运行期上下文。属性点客户端未按名透出 → 传 -1(seam)。
|
||||
# EPointTypes(服务器 char.h):ST=12 HT=13 DX=14 IQ=15;MOUNT=109(骑乘中坐骑 vnum)。
|
||||
const POINT_ST := 12
|
||||
const POINT_HT := 13
|
||||
const POINT_DX := 14
|
||||
const POINT_IQ := 15
|
||||
const POINT_MOUNT := 109
|
||||
|
||||
# 组装 EquipRules.can_equip 需要的运行期上下文(服务器真实链所需字段全集)。
|
||||
# 属性四维从 get_points()["points"] 全量数组取;worn 是 {wear_cell: vnum} 穿戴表,
|
||||
# 供 RING/UNIQUE 占用翻转与 UNIQUE 同组去重;polymorphed/riding 供 EquipItem 后置门。
|
||||
func _equip_ctx() -> Dictionary:
|
||||
var ctx := {"race": 0, "empire": 0, "level": 0, "st": -1, "dx": -1, "ht": -1, "iq": -1}
|
||||
var ctx := {"race": 0, "empire": 0, "level": 0, "st": -1, "dx": -1, "ht": -1, "iq": -1,
|
||||
"worn": {}, "polymorphed": false, "riding": false}
|
||||
if client == null:
|
||||
return ctx
|
||||
if client.has_method("get_points"):
|
||||
var p: Dictionary = client.get_points()
|
||||
ctx["level"] = int(p.get("level", 0))
|
||||
var vals: Array = p.get("points", [])
|
||||
if vals.size() > POINT_MOUNT:
|
||||
ctx["st"] = int(vals[POINT_ST])
|
||||
ctx["ht"] = int(vals[POINT_HT])
|
||||
ctx["dx"] = int(vals[POINT_DX])
|
||||
ctx["iq"] = int(vals[POINT_IQ])
|
||||
ctx["riding"] = int(vals[POINT_MOUNT]) > 0
|
||||
# 穿戴表 {wear_cell: vnum}:旧 11 格来自 get_equipment()(下标即 wear cell);
|
||||
# 新装备槽 19..23(costume/ring/belt)留在 INVENTORY 全局 cell 90+wear(见上方常量)。
|
||||
var worn := {}
|
||||
if client.has_method("get_equipment"):
|
||||
var eq: Array = client.get_equipment()
|
||||
for i in eq.size():
|
||||
var vnum := int(eq[i].get("vnum", 0)) if eq[i] is Dictionary else 0
|
||||
if vnum != 0:
|
||||
worn[i] = vnum
|
||||
if client.has_method("get_inventory"):
|
||||
for it in client.get_inventory():
|
||||
if it is Dictionary:
|
||||
var cell := int(it.get("cell", -1))
|
||||
if cell >= EQUIP_BASE + WEAR_COSTUME_BODY and cell < EQUIP_BASE + WEAR_COUNT:
|
||||
worn[cell - EQUIP_BASE] = int(it.get("vnum", 0))
|
||||
ctx["worn"] = worn
|
||||
if client.has_method("get_entities"):
|
||||
for e in client.get_entities():
|
||||
if bool(e.get("is_main", false)):
|
||||
@@ -681,6 +714,8 @@ func _equip_ctx() -> Dictionary:
|
||||
ctx["empire"] = int(e.get("empire", 0))
|
||||
if int(e.get("level", 0)) > 0:
|
||||
ctx["level"] = int(e.get("level", 0))
|
||||
ctx["polymorphed"] = int(e.get("polymorph", 0)) != 0 \
|
||||
or int(e.get("poly_vnum", 0)) != 0
|
||||
break
|
||||
return ctx
|
||||
|
||||
@@ -689,6 +724,10 @@ func _equip_reject_hint(verdict: Dictionary) -> String:
|
||||
"ANTI_SEX": return "该装备不符合你的性别"
|
||||
"ANTI_JOB": return "该装备不符合你的职业"
|
||||
"ANTI_EMPIRE": return "该装备不符合你的帝国"
|
||||
"UNIQUE_DUP": return "同类特殊装备只能佩戴一件"
|
||||
"POLYMORPH": return "变身状态下无法更换装备"
|
||||
"RIDE_TUXEDO": return "骑乘状态下无法穿礼服"
|
||||
"RIDE_ITEM": return "已经在骑乘中了"
|
||||
"NOT_WEARABLE_HERE": return "该装备不能穿在这个位置"
|
||||
"LIMIT_LEVEL": return "需要等级 %d" % int(verdict.get("need", 0))
|
||||
"LIMIT_STR": return "需要力量 %d" % int(verdict.get("need", 0))
|
||||
|
||||
@@ -223,6 +223,10 @@ func format(vnum: int, count: int, proto_data: Dictionary, instance_data: Dictio
|
||||
for limit in proto_data.get("limits", []):
|
||||
var limit_type := int(limit.get("type", 0))
|
||||
var limit_value := int(limit.get("value", 0))
|
||||
if limit_type == 6:
|
||||
# LIMIT_PCBANG 只是“网吧专用”标记,不是装备门禁(服务器 item.cpp:1899)。
|
||||
lines.append("PC房专用道具")
|
||||
continue
|
||||
if limit_type != 0 and limit_value > 0:
|
||||
lines.append("%s:%d" % [LIMIT_NAMES.get(limit_type, "限制 %d" % limit_type), limit_value])
|
||||
for apply in proto_data.get("applies", []):
|
||||
@@ -291,19 +295,32 @@ func _append_realtime_limit(lines: Array[String], proto_data: Dictionary, socket
|
||||
return
|
||||
var value := int(sockets[0])
|
||||
var remaining := 0
|
||||
if limit_type == 7 or limit_type == 8:
|
||||
if limit_type == 8:
|
||||
# REAL_TIME_START_FIRST_USE:socket1 = 使用计数。未启用(0)时 socket0 是
|
||||
# 时长秒数而非到期时间戳;启用后服务器把 socket0 换成到期 unix ts
|
||||
#(char_item.cpp:6100 尾部 cLimitRealTimeFirstUseIndex 分支)。
|
||||
var used := int(sockets[1]) if sockets.size() > 1 else 0
|
||||
if used == 0:
|
||||
lines.append("首次使用后计时:%s" % _duration_text(value))
|
||||
return
|
||||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||||
elif limit_type == 7:
|
||||
# REAL_TIME:socket0 = 到期 unix ts(item_length.h:328-347)。
|
||||
remaining = maxi(0, value - int(Time.get_unix_time_from_system()))
|
||||
else:
|
||||
remaining = maxi(0, value)
|
||||
if remaining <= 0:
|
||||
lines.append("剩余时间:已超时")
|
||||
return
|
||||
var minutes := remaining / 60
|
||||
var seconds := remaining % 60
|
||||
var text := "%d秒" % seconds
|
||||
lines.append("剩余时间:%s" % _duration_text(remaining))
|
||||
|
||||
func _duration_text(seconds: int) -> String:
|
||||
var minutes := seconds / 60
|
||||
var rest := seconds % 60
|
||||
var text := "%d秒" % rest
|
||||
if minutes > 0:
|
||||
text = "%d分%s" % [minutes, text] if seconds > 0 else "%d分" % minutes
|
||||
lines.append("剩余时间:%s" % text)
|
||||
text = "%d分%s" % [minutes, text] if rest > 0 else "%d分" % minutes
|
||||
return text
|
||||
|
||||
func _append_special_item_data(lines: Array[String], item_type: int, vnum: int,
|
||||
values: Array, proto_data: Dictionary, instance_data: Dictionary) -> void:
|
||||
|
||||
@@ -19,10 +19,17 @@ func refresh() -> void:
|
||||
return
|
||||
var lover: Dictionary = client.get_lover()
|
||||
var valid := bool(lover.get("valid", false))
|
||||
# lover_divorce(CHAT_TYPE_COMMAND)→ store 清空 LoverInfo → valid=false 自动隐藏。
|
||||
_root.visible = valid and not _mobile_mode
|
||||
if not valid:
|
||||
return
|
||||
_name.text = "♥ " + String(lover.get("name", ""))
|
||||
# 上/下线与靠近/远离由服务器 lover_login/logout/near/far 命令驱动
|
||||
#(marriage.cpp;原版 affectShower 只在“靠近”时亮心形)。
|
||||
var online := bool(lover.get("online", false))
|
||||
var near := bool(lover.get("near", false))
|
||||
var state := "(在身边)" if near else "(在线)" if online else "(离线)"
|
||||
_name.text = "♥ " + String(lover.get("name", "")) + state
|
||||
_name.modulate = Color(1.0, 0.6, 0.72) if near else Color(1.0, 0.72, 0.82)
|
||||
_bar.value = clampi(int(lover.get("love_point", 0)), 0, 100)
|
||||
_bar.tooltip_text = "爱意值:%d%%" % int(_bar.value)
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ func get_mobile_window() -> Control:
|
||||
return _root
|
||||
|
||||
func close() -> void:
|
||||
# 对齐原版 MallWindow.Close():先发 /mall_close 让服务器关掉商城仓库。
|
||||
if client and client.has_method("mall_close") and is_open():
|
||||
client.mall_close()
|
||||
if item_mouse and item_mouse.has_method("unregister_owner"):
|
||||
item_mouse.unregister_owner(self)
|
||||
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
|
||||
|
||||
+56
-1
@@ -65,7 +65,19 @@ func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter:
|
||||
client.quickslots_changed.connect(restore_from_server)
|
||||
if client.has_signal("skills_changed"):
|
||||
client.skills_changed.connect(_refresh_page)
|
||||
restore_from_server()
|
||||
# 物品槽引用的是背包 cell:装备/销毁/丢弃后服务器 SyncQuickslot 会推
|
||||
# quickslots_changed;这里再挂 inventory_changed 兜住时序窗口(置灰失效引用)。
|
||||
if client.has_signal("inventory_changed"):
|
||||
client.inventory_changed.connect(func(_w, _c): _refresh_page())
|
||||
# 死亡/复活:死亡中槽位全部置灰(对齐原版 POS_DEAD 全链路禁用)。
|
||||
if client.has_signal("entity_dead"):
|
||||
client.entity_dead.connect(_on_entity_dead)
|
||||
# 变身(AFFECT_POLYMORPH=220):技能槽整体遮罩,结束(re-broadcast)后解除。
|
||||
if client.has_signal("affect_added"):
|
||||
client.affect_added.connect(_on_affect_added)
|
||||
if client.has_signal("affect_removed"):
|
||||
client.affect_removed.connect(_on_affect_removed)
|
||||
restore_from_server()
|
||||
|
||||
# 从服务器 GC_QUICKSLOT_* 恢复全部 36 个快捷栏槽位。
|
||||
# 参考 ESlotType:1 道具 / 2 技能 / 3 表情 / 4 商店(不可执行)。
|
||||
@@ -140,6 +152,9 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
return
|
||||
if _now() < s.cd_end:
|
||||
return
|
||||
# 死亡中不可使用(服务器 POS_DEAD 拦截一切使用;这里提前本地拦截)。
|
||||
if _player_dead():
|
||||
return
|
||||
if s.kind == "skill":
|
||||
var mi: int = table.motion_idx_of(s.id) if table else 0
|
||||
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
|
||||
@@ -220,6 +235,35 @@ func _aim_heading(player_node: Node3D, screen_direction: Vector2, fallback: floa
|
||||
var camera_delta := rad_to_deg(camera_heading - player_node.rotation.y)
|
||||
return fposmod(fallback + camera_delta + angle_from_up, 360.0)
|
||||
|
||||
# 服务器权威的死亡态(GC add chr state DEAD);复活后 entity_info 会再触发刷新。
|
||||
func _player_dead() -> bool:
|
||||
if client == null or not client.has_method("get_main_vid"):
|
||||
return false
|
||||
var me: Dictionary = client.get_entity(client.get_main_vid())
|
||||
return me != null and bool(me.get("dead", false))
|
||||
|
||||
# 变身态(POINT_POLYMORPH / affect 220):技能槽遮罩的数据源。
|
||||
func _polymorphed() -> bool:
|
||||
if client == null or not client.has_method("get_main_vid"):
|
||||
return false
|
||||
var me: Dictionary = client.get_entity(client.get_main_vid())
|
||||
return me != null and (int(me.get("polymorph", 0)) != 0 or int(me.get("poly_vnum", 0)) != 0)
|
||||
|
||||
func _on_entity_dead(vid: int) -> void:
|
||||
if client and client.has_method("get_main_vid") and vid == client.get_main_vid():
|
||||
_refresh_page()
|
||||
|
||||
# 只关心 AFFECT_POLYMORPH(220) 的增减,避免每次 buff 变化都整页重刷。
|
||||
const AFFECT_POLYMORPH := 220
|
||||
|
||||
func _on_affect_added(affect: Dictionary) -> void:
|
||||
if int(affect.get("type", 0)) == AFFECT_POLYMORPH:
|
||||
_refresh_page()
|
||||
|
||||
func _on_affect_removed(type: int) -> void:
|
||||
if type == AFFECT_POLYMORPH:
|
||||
_refresh_page()
|
||||
|
||||
# net_play 的 MODE_USE_SKILL 预约进射程后回调(use_skill_hook)——slot 为全局快捷栏
|
||||
# 索引。返回是否确实发起了施法(对齐参考端 `if (__UseSkill(slot)) __ClearReservedAction()`)。
|
||||
func activate_reserved(global_slot: int) -> bool:
|
||||
@@ -422,9 +466,20 @@ func _refresh_slot(slot: int) -> void:
|
||||
grade_text = _skill_grade_text(int(s.id))
|
||||
elif s.kind == "item":
|
||||
txt = "#%d" % s.id
|
||||
# 引用失效(物品已装备/销毁/掉落,服务器 SyncQuickslot 尚未推或已删)→ 置灰。
|
||||
var dim := false
|
||||
if s.kind == "item" and client and client.has_method("get_item"):
|
||||
var it: Dictionary = client.get_item(1, s.id)
|
||||
if it.is_empty() or int(it.get("vnum", 0)) == 0:
|
||||
dim = true
|
||||
txt = "×%d" % s.id
|
||||
# 变身中技能槽整体遮罩(服务器只放行箭矢类物品);死亡中全部置灰。
|
||||
if (s.kind == "skill" and _polymorphed()) or _player_dead():
|
||||
dim = true
|
||||
var icon: TextureRect = _slots[slot].icon
|
||||
icon.texture = tex
|
||||
icon.visible = tex != null
|
||||
_slots[slot].btn.modulate = Color(0.42, 0.42, 0.42) if dim else Color(1, 1, 1)
|
||||
_slots[slot].lbl.text = txt if tex == null else ""
|
||||
_slots[slot].grade.text = grade_text
|
||||
_slots[slot].grade.visible = grade_text != "" and tex != null
|
||||
|
||||
@@ -30,6 +30,7 @@ var _mobile_inventory_title: Label
|
||||
var _mobile_inventory_scroll: ScrollContainer
|
||||
var _mobile_inventory_list: VBoxContainer
|
||||
var _mobile_deposit_pending := {}
|
||||
var _change_pw_dialog: ConfirmationDialog
|
||||
|
||||
const SAFE_PAGE_SLOTS := 45
|
||||
const SAFE_WINDOW := 3
|
||||
@@ -62,6 +63,10 @@ func get_mobile_window() -> Control:
|
||||
return _root
|
||||
|
||||
func close() -> void:
|
||||
# 对齐原版 SafeboxWindow.Close():先发 /safebox_close 让服务器同步关仓
|
||||
#(CloseSafebox 命令回来后 is_safebox_open() 变 false),本地只是立即收尾。
|
||||
if client and client.has_method("safebox_close") and is_open():
|
||||
client.safebox_close()
|
||||
if item_mouse and item_mouse.has_method("unregister_owner"):
|
||||
item_mouse.unregister_owner(self)
|
||||
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
|
||||
@@ -130,7 +135,9 @@ func refresh() -> void:
|
||||
_page = clampi(_page, 0, pages - 1)
|
||||
_title.text = "仓库(第 %d/%d 页)" % [_page + 1, pages]
|
||||
_page_label.text = "%d / %d" % [_page + 1, pages]
|
||||
_gold.text = "仓库金币: %d" % client.get_safebox_gold()
|
||||
# 40250 无仓库金钱存取协议(HEADER_CG_SAFEBOX_MONEY 不存在,GC 84 从不发送,
|
||||
# 原版 RefreshSafeboxMoney 也是 pass)——只读展示,通常恒 0。
|
||||
_gold.text = "仓库金币: %d(本版本不可存取)" % client.get_safebox_gold()
|
||||
for c in _list.get_children():
|
||||
c.queue_free()
|
||||
_render_grid()
|
||||
@@ -302,6 +309,67 @@ func _ask_password(kind: String) -> void:
|
||||
else:
|
||||
_password_dialog.popup_centered()
|
||||
|
||||
# 修改仓库密码:/safebox_change_password <旧> <新>(服务器两个参数各限 1..6 位)。
|
||||
func _ask_change_password() -> void:
|
||||
if not is_open():
|
||||
return
|
||||
if is_instance_valid(_change_pw_dialog):
|
||||
_change_pw_dialog.queue_free()
|
||||
_change_pw_dialog = ConfirmationDialog.new()
|
||||
_change_pw_dialog.title = "修改仓库密码"
|
||||
_change_pw_dialog.dialog_text = "旧密码 / 新密码(1~6 位)/ 再输入一次新密码"
|
||||
_change_pw_dialog.ok_button_text = "确认"
|
||||
_change_pw_dialog.cancel_button_text = "取消"
|
||||
var box := VBoxContainer.new()
|
||||
var edits: Array[LineEdit] = []
|
||||
for hint in ["旧密码", "新密码", "确认新密码"]:
|
||||
var row := HBoxContainer.new()
|
||||
var lbl := Label.new()
|
||||
lbl.text = hint
|
||||
lbl.custom_minimum_size = Vector2(96, 0)
|
||||
row.add_child(lbl)
|
||||
var edit := LineEdit.new()
|
||||
edit.secret = true
|
||||
edit.max_length = 6
|
||||
edit.placeholder_text = hint
|
||||
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
|
||||
edit.custom_minimum_size = Vector2(360, 44) if _mobile_mode else Vector2(160, 28)
|
||||
row.add_child(edit)
|
||||
box.add_child(row)
|
||||
edits.append(edit)
|
||||
_change_pw_dialog.add_child(box)
|
||||
_change_pw_dialog.confirmed.connect(func() -> void:
|
||||
var old_pw := edits[0].text
|
||||
var new_pw := edits[1].text
|
||||
if new_pw != edits[2].text:
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "两次输入的新密码不一致"
|
||||
elif old_pw.is_empty() or new_pw.is_empty() or new_pw.length() > 6 \
|
||||
or old_pw.length() > 6:
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "密码须为 1~6 位"
|
||||
elif not client.safebox_change_password(old_pw, new_pw):
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "修改密码请求发送失败"
|
||||
else:
|
||||
if is_instance_valid(_status):
|
||||
_status.text = "已提交修改密码请求"
|
||||
_change_pw_dialog.queue_free()
|
||||
_change_pw_dialog = null)
|
||||
_change_pw_dialog.canceled.connect(func() -> void:
|
||||
_change_pw_dialog.queue_free()
|
||||
_change_pw_dialog = null)
|
||||
if _mobile_mode:
|
||||
_change_pw_dialog.set_meta("mobile_modal", true)
|
||||
_change_pw_dialog.set_meta("mobile_title", "修改仓库密码")
|
||||
_change_pw_dialog.min_size = Vector2i(560, 320)
|
||||
_change_pw_dialog.size = Vector2i(560, 320)
|
||||
_root.add_child(_change_pw_dialog)
|
||||
if _mobile_mode:
|
||||
_change_pw_dialog.popup_centered(Vector2i(560, 320))
|
||||
else:
|
||||
_change_pw_dialog.popup_centered()
|
||||
|
||||
func _row(it: Dictionary) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(300, 0)
|
||||
@@ -368,6 +436,13 @@ func _build(parent: Node) -> void:
|
||||
next.size = Vector2(28, 24)
|
||||
next.pressed.connect(func(): _change_page(1))
|
||||
_root.add_child(next)
|
||||
var change_pw := Button.new()
|
||||
change_pw.text = "改密"
|
||||
change_pw.position = Vector2(186, 6)
|
||||
change_pw.size = Vector2(42, 24)
|
||||
change_pw.add_theme_font_size_override("font_size", 11)
|
||||
change_pw.pressed.connect(_ask_change_password)
|
||||
_root.add_child(change_pw)
|
||||
_gold = Label.new()
|
||||
_gold.text = "仓库金币: 0"
|
||||
_gold.position = Vector2(12, 30)
|
||||
|
||||
Reference in New Issue
Block a user