Files
mtgodot-poc/project/gamescene_test.gd
T
shenleiandClaude Opus 5 1933a5ceda fix(combat): 战斗逐项对齐 40250,修复移动 / 技能释放与武器挂点
- 连击类型由 combo_changed 驱动(SetComboSkillFlag,技能 122 等级)
- 命中判定改为 .msa 刀尖 Z 圆柱 vs .msm 防御球(hit_collision.gd 逐行移植)
- _register_hit 改为 map::insert 语义,修正命中上限
- 攻速同时缩放 GetAttackingElapsedTime 与攻击动作速率;按武器动作模式目录绑定攻击段
- 去掉本地连击超时,motion_bound 清命中表 / 连击段号
- __ProcessDataAttackSuccess:InsertDelay 硬直、physics_push.gd 击退 + GetBlendingPosition 同步、
  命中特效、__HitGood / __HitGreate / __HitStone 受击动作链与抖动(ui/hit_reaction.gd)
- ClassicSession::send_use_skill 不再夹带 CG_FLY_TARGETING
- mac 前进 / 后退方向与技能释放修复;武器按职业骨骼挂到手上
- 新增 hit_collision / physics_push / hit_view / net_world_push / weapon_attach 测试;
  gpu_pose_bounds / race_motion_assembly 适配 damage 随机变体
- 文档:CLIENT-GAP.md、CLIENT-GAP-FIX.md §3.5 / §3.7 与 C.5 增量 130

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
2026-09-12 13:15:28 +09:00

144 lines
6.2 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.
# gamescene_test —— game_scene.gd 装配的 headless 自检(无服务器 / 无地图)。
# godot --headless --path project --script gamescene_test.gd
# 假 M2Client 驱动:断言场景装出来、子系统接线、实体镜像、主角定位。
extends SceneTree
const GameScene = preload("res://game_scene.gd")
const MobileUiRoot = preload("res://ui/mobile/mobile_ui_root.gd")
class FakeClient extends Node:
signal entity_spawned(entity: Dictionary)
signal entity_despawned(vid: int)
signal entity_moved(vid: int)
signal entity_main_set(vid: int)
signal entity_dead(vid: int)
signal vitals_changed(vid: int)
signal points_changed(points: Dictionary)
signal target_info(vid: int, hp_percent: int)
signal chat(type: int, vid: int, text: String)
signal damage(vid: int, amount: int, flag: int)
signal motion(vid: int, victim_vid: int, motion: int)
signal gift_available()
signal guild_make_requested()
signal script_dialog(skin: int, text: String)
var ents := {}
var main := 0
func is_in_game() -> bool: return true
func get_main_vid() -> int: return main
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
func move(_f, _a, _r, _x, _y) -> bool: return true
func attack(_m, _v) -> bool: return true
func set_target(_v) -> bool: return true
func guild_answer_make(_name: String) -> bool: return true
func spawn(vid, nm, pos, is_main := false):
ents[vid] = {"vid": vid, "name": nm, "pos": pos, "is_main": is_main,
"func": 0, "moving": false, "angle_deg": 0.0, "hp": 100, "max_hp": 100, "dead": false}
if is_main:
main = vid
entity_main_set.emit(vid)
entity_spawned.emit(ents[vid])
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
await _run()
if _fail == 0:
print("PASS: gamescene_test (assembly + wiring + entity mirror + spawn locate)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var fc := FakeClient.new()
get_root().add_child(fc)
# 主角先于场景就位(模拟重连 / 已在局内)
fc.spawn(1000, "Me", Vector3(20, 0, -30), true)
var gs: Node3D = GameScene.new()
get_root().add_child(gs)
await gs.setup(fc, "", "no_such_map") # setup 现在是协程(分帧避免阻塞漏 PONG)
await process_frame
# 1) 子系统都装出来了
_ck(gs.player != null, "player built")
_ck(gs.cam != null and gs.cam.current, "camera current")
_ck(gs.pc != null, "player_controller built")
_ck(gs.net_world != null, "net_world built")
_ck(gs.net_play != null, "net_play built")
# §3.7 命中特效 m_dwBattleHitEffectIDnet_play 用场景的特效注册表,挂在实体挂载点下
_ck(gs.net_play != null and gs.fx != null and gs.net_play.fx == gs.fx \
and gs.net_play.fx_parent == gs.get_node_or_null("Entities"),
"net_play.fx / fx_parent wired to the scene effect registry + entity mount")
_ck(gs.hud != null, "hud built")
_ck(gs.get_node_or_null("AppLifecycle") == null,
"GameScene does not create a second lifecycle coordinator")
_ck(gs.lifecycle_audio() != null and gs.lifecycle_audio().get_parent() == gs,
"GameScene exposes its audio to AppFlow lifecycle owner")
_ck(gs.dragon_soul_ui == null, "Dragon Soul runtime entry disabled for target 40250")
_ck(gs.atlas_ui != null, "atlas_ui built")
_ck(gs.get_node_or_null("Entities") != null, "entity mount present")
# 2) 主角定位到出生点(load_map 失败 -> sample_height 返回 0 -> y=0
_ck(gs.player.position.distance_to(Vector3(20, 0, 30)) < 0.01, # MapCoord.to_world 翻 Z
"local player placed at spawn (world 帧), got %s" % gs.player.position)
# 3) net_world 不给主角画节点(本地 player 代表)
await process_frame
_ck(gs.net_world.node_for(1000) == null, "no net_world node for local vid")
fc.motion.emit(1000, 0, 305) # CRaceMotionData::NAME_CLAP
await process_frame
_ck(int(gs.player.get_meta("last_motion", 0)) == 305,
"local GC_MOTION reaches the player representative")
fc.gift_available.emit()
await process_frame
_ck(gs._gift_dialog != null and gs._gift_dialog.visible,
"gift server command opens a visible notification")
gs._gift_dialog.hide() # 关闭已有 modal,模拟玩家已处理上一条通知
fc.guild_make_requested.emit()
await process_frame
_ck(gs.guild_ui._create_dialog != null and gs.guild_ui._create_dialog.visible,
"guild make request reaches the in-game guild creation dialog")
gs.guild_ui._close_guild_create_dialog()
# Quest EventManager side effects are consumed by the scene owner, not left
# as parser-only signals.
fc.script_dialog.emit(0, "[SET_CAMERA x(1000) y(2000) z(300) distance(500) pitch(40) rot(90)][DUNGEON_RESULT killstone_count(2) killmob_count(40) find_hidden(1) hidden_total(3) use_potion(5) is_revived(0) killallmob(1) total_time(600) bonus_exp(1234)]")
await process_frame
_ck(gs.cam.has_method("is_event_locked") and bool(gs.cam.call("is_event_locked")),
"quest SET_CAMERA reaches the scene camera")
_ck(gs.dungeon_result_ui != null and bool(gs.dungeon_result_ui.call("is_open")),
"quest DUNGEON_RESULT opens the scene result window")
# 4) 别的实体会被镜像成挂载节点
fc.spawn(2000, "Wolf", Vector3(25, 0, -30))
await process_frame
var mob: Node3D = gs.net_world.node_for(2000)
_ck(mob != null and mob.get_parent() == gs.get_node("Entities"), "remote entity mirrored under mount")
# 5) HUD 活着,points 驱动不报错
fc.points_changed.emit({"hp": 60, "max_hp": 100, "sp": 20, "max_sp": 50,
"exp": 10, "next_exp": 100, "level": 3})
await process_frame
_ck(true, "points_changed handled without error")
gs.queue_free()
await process_frame
# Mobile profile uses the same scene assembly and NetPlay HUD contract, but
# swaps only the presentation root. This also guards against accidentally
# constructing the desktop HUD alongside the touch layer.
ProjectSettings.set_setting("mt/ui/profile", "mobile")
var mobile_gs: Node3D = GameScene.new()
get_root().add_child(mobile_gs)
await mobile_gs.setup(fc, "", "no_such_map")
await process_frame
_ck(mobile_gs.hud is MobileUiRoot, "mobile profile selects MobileUiRoot")
_ck(mobile_gs.hud.input_overlay != null, "mobile profile creates touch overlay")
mobile_gs.queue_free()
ProjectSettings.set_setting("mt/ui/profile", "auto")