125 lines
5.1 KiB
GDScript
125 lines
5.1 KiB
GDScript
# gamescene_test —— game_scene.gd 装配的 headless 自检(无服务器 / 无地图)。
|
||
# godot --headless --path project --script gamescene_test.gd
|
||
# 假 M2Client 驱动:断言场景装出来、子系统接线、实体镜像、主角定位。
|
||
extends SceneTree
|
||
|
||
const GameScene = preload("res://game_scene.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")
|
||
_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()
|