Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
93 lines
3.3 KiB
GDScript
93 lines
3.3 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)
|
||
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 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.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")
|
||
|
||
# 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()
|