Complete remaining client gap features

This commit is contained in:
shenlei
2026-09-04 13:25:05 +09:00
parent 13ccb8d02d
commit 9a4a044da5
47 changed files with 6667 additions and 140 deletions
+79 -9
View File
@@ -16,6 +16,9 @@
extends Node
signal target_changed(vid: int)
# game.py SetPCTargetBoard :804 —— 按住 LCONTROL 点同阵营他人 PC 时请求开悄悄话对话框
# (参考端 self.interface.OpenWhisperDialog(name))。game_scene 接到 chat.start_whisper()。
signal whisper_requested(name: String)
# OnCannotAttack / OnCannotShot / OnCannotUseSkill 的字符串码(game.py 同名回调),
# 供 HUD 弹 localeInfo 文案。§3.4
signal cannot_act(code: String)
@@ -61,6 +64,8 @@ const CM := 100.0 # 米 -> 厘米
# §5.4 —— PVP / PK / 决斗可攻击判据(CInstanceBase::IsAttackableInstance 1:1,逐行)。
const EntityRules = preload("res://entity_rules.gd")
# §5.4 / §8.8 —— RecvTargetPacket 的目标框收包分派(判定树 + CanViewTargetHP1:1。
const TargetBoard = preload("res://target_board.gd")
# §3.2 上行节流表(对齐 `PythonPlayerEventHandler.cpp` 的 6 个回调):
# OnMove —— 首次开走 / 改向:立即发 FUNC_MOVE,并重置 moving/waiting 两个窗口
# OnMoving —— 移动中周期上行:300 ms 节流,发 FUNC_MOVE
@@ -162,6 +167,12 @@ const NAME_COMBO_ATTACK_1 := 14
var _target_vid := 0
var _main_vid := 0
# game-option「看他国玩家目标框」(constinfo VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD,默认 1)。
# 关掉时 TargetBoard.Open 前置过滤:异阵营 PC 目标框直接不开(uitarget.py:189-193)。
var _view_other_empire_target := true
# game.py SetPCTargetBoard :807 —— app.IsPressed(app.DIK_LCONTROL)。poc 侧由 game_scene
# 的 KEY_CTRL 按下 / 松开喂 set_lcontrol_down(),点 PC 时轮询(对齐参考端 IsPressed 语义)。
var _lcontrol_down := false
var _last_sent_pos := Vector3.ZERO # 上一次上行时的玩家世界坐标
var _last_moving_sent_t := 0.0 # 上一次 OnMoving 上行时刻(秒)
var _last_waiting_sent_t := 0.0 # 上一次 OnWaiting 上行时刻(秒)
@@ -502,13 +513,22 @@ func _heading_deg(yaw_rad: float) -> float:
func _player_yaw() -> float:
return pc.player.rotation.y if pc.player else 0.0
# CHRTYPE 分类:0 PC / 1 NPC / 2 MONSTER / 3 STONE / 4 WARP
# CHRTYPE 分类:0 PC / 1 NPC / 2 MONSTER / 3 STONE / 4 WARP / 5 BUILDING /
# 6 WOODEN_DOOR。门类型既可由实体 fixture 的 building / kind 给出,也可由
# mob_proto 的 bType=4 + 13000 / 30111..30119 race 识别。
# 本 fork 的 GC_CHARACTER_ADD.bType 对 NPC/怪都是 0,按 race 查 mob_proto 补。
func _entity_kind(e: Dictionary) -> int:
if bool(e.get("building", false)):
return EntityRules.KIND_BUILDING
var explicit_kind := int(e.get("kind", -1))
if explicit_kind >= EntityRules.KIND_BUILDING:
return explicit_kind
var ct := int(e.get("ch_type", 0))
if ct != 0:
return ct
var race := int(e.get("race", 0))
if EntityRules._is_wooden_door(e):
return EntityRules.KIND_WOODEN_DOOR
if race >= 1 and proto and proto.has_method("mob"):
var m: Dictionary = proto.mob(race)
if not m.is_empty():
@@ -518,6 +538,8 @@ func _entity_kind(e: Dictionary) -> int:
1: return 1
2: return 3
3: return 4
4: return EntityRules.KIND_WOODEN_DOOR \
if EntityRules._is_wooden_door(e) else EntityRules.KIND_BUILDING
return 0
# 实体显示名:玩家 / NPC 走 GC_CHAR_ADD_INFO 的 name;怪没有名字包 -> 查 mob_proto。
@@ -697,16 +719,64 @@ func _on_pick(node: Node3D) -> void:
return
_attack_cd = 0.0
_change_target_to_picked_instance(vid)
# game.py SetPCTargetBoard :804 —— 点到 PC 时 targetBoard.Open 之后,若按住 LCONTROL
# 且同阵营(:809 IsSameEmpire)且非本人(:812 IsMainCharacterIndex)且非 building
# :814 INSTANCE_TYPE_BUILDING)→ interface.OpenWhisperDialog(name)。C++ 侧只对 PC
# 分派 SetPCTargetBoard,故 poc 也只在 KIND_PC 分支查。建筑 / 木门由实体分类链识别,
# 不满足 PC 分支时不会误触发悄悄话。
if _entity_kind(e) == EntityRules.KIND_PC:
var same_empire := EntityRules._is_same_empire(_main_entity(), e)
if TargetBoard.whisper_on_ctrl_click(_lcontrol_down, same_empire, vid == _main_vid, false):
var nm := _entity_name(e)
if nm != "":
whisper_requested.emit(nm)
# poc 版 RecvTargetPacketPythonNetworkStreamPhaseGame.cpp:2424):C++ 侧从 EntityStore
# 抽出 (vid, hp%),这里照参考端的判定树分派目标框——目标实例缺失 -> 关框;已死透 -> 不动框;
# PC / building 目标 -> 只在框上是别的 VID 时关掉(不显血条);石头 / 敌对怪等「可看血」
# 目标 -> SetHPTargetBoardNPC / warp 等不可看血目标 -> 关框。经 target_board.gd 纯静态镜像。
func _on_target_info(vid: int, hp_pct: int) -> void:
if vid == 0:
_clear_target()
return
_target_vid = vid
if hud and hud.has_method("set_target"):
var e: Dictionary = client.get_entity(vid)
var nm := _entity_name(e) if not e.is_empty() else ""
hud.set_target(nm if nm != "" else "目标", hp_pct)
var e: Dictionary = client.get_entity(vid) if vid != 0 else {}
var exists := vid != 0 and not e.is_empty()
var dead := exists and bool(e.get("dead", false))
var kind := _entity_kind(e) if exists else -1
var is_pc := kind == EntityRules.KIND_PC
var is_building := EntityRules._is_building(e)
# uitarget.py:189-193 前置过滤:game-option 关「看他国目标框」时异阵营目标不开框。
# empire 0 的怪/NPC/石头在 _is_same_empire 里算同阵营 → 不受影响,只挡真·他国 PC。
if exists:
var same_empire := EntityRules._is_same_empire(_main_entity(), e)
if TargetBoard.hidden_as_other_empire(_view_other_empire_target, same_empire):
_clear_target()
return
var can_view := TargetBoard.can_view_target_hp(
kind == EntityRules.KIND_STONE, EntityRules._is_wooden_door(e),
kind == EntityRules.KIND_MONSTER)
match TargetBoard.classify(exists, dead, is_pc, is_building, can_view):
TargetBoard.Action.CLOSE:
_clear_target()
TargetBoard.Action.CLOSE_IF_DIFFERENT:
if TargetBoard.should_close_if_different(vid, _target_vid):
_clear_target()
TargetBoard.Action.SET_HP:
if TargetBoard.needs_reset(vid, _target_vid):
_target_vid = vid
target_changed.emit(vid)
if hud and hud.has_method("set_target"):
var nm := _entity_name(e)
hud.set_target(nm if nm != "" else "目标",
TargetBoard.clamp_hp_pct(hp_pct))
TargetBoard.Action.NONE:
pass
# game_option_ui 的「看他国玩家目标框」radiodisplay_option_changed "target_board")。
# uigameoption.py → constInfo.SET_VIEW_OTHER_EMPIRE_PLAYER_TARGET_BOARD(index)。
func set_view_other_empire_target(on: bool) -> void:
_view_other_empire_target = on
# game_scene 的 KEY_CTRL 按下 / 松开 → app.IsPressed(app.DIK_LCONTROL) 语义(game.py:807)。
func set_lcontrol_down(down: bool) -> void:
_lcontrol_down = down
func _clear_target() -> void:
if _target_vid == 0: