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
132 lines
4.8 KiB
GDScript
132 lines
4.8 KiB
GDScript
# p9_test —— P9:小地图 + 昼夜 + 天气 + 传送 headless 自检。
|
||
# godot --headless --path project --script p9_test.gd
|
||
extends SceneTree
|
||
|
||
const Minimap = preload("res://ui/minimap.gd")
|
||
const WorldTime = preload("res://world/world_time.gd")
|
||
const Weather = preload("res://fx/weather.gd")
|
||
|
||
class FakeClient extends Node:
|
||
signal time_changed(server_epoch: int)
|
||
signal channel_changed(channel: int)
|
||
signal npc_marks_changed()
|
||
signal land_areas_changed()
|
||
signal observer_event(kind: int, vid: int, pos: Vector2)
|
||
signal world_markers_changed()
|
||
signal warp(pos: Vector3, same_server: bool)
|
||
var _time := 0
|
||
var entities := []
|
||
var npc_marks := []
|
||
var markers := []
|
||
var observers := []
|
||
var land_areas := []
|
||
var party := []
|
||
func get_server_time() -> int: return _time
|
||
func get_entities() -> Array: return entities
|
||
func get_npc_marks() -> Array: return npc_marks
|
||
func get_world_markers() -> Array: return markers
|
||
func get_observers() -> Array: return observers
|
||
func get_land_areas() -> Array: return land_areas
|
||
func get_party() -> Array: return party
|
||
func get_channel() -> int: return 3
|
||
|
||
var _fail := 0
|
||
func _ck(c: bool, m: String) -> void:
|
||
if not c:
|
||
_fail += 1
|
||
printerr("FAIL: " + m)
|
||
|
||
func _init() -> void:
|
||
_run()
|
||
if _fail == 0:
|
||
print("PASS: p9_test (minimap + world_time + weather + warp)")
|
||
quit(0)
|
||
else:
|
||
printerr("%d check(s) failed" % _fail)
|
||
quit(1)
|
||
|
||
func _run() -> void:
|
||
var canvas := CanvasLayer.new()
|
||
get_root().add_child(canvas)
|
||
var fc := FakeClient.new()
|
||
get_root().add_child(fc)
|
||
var player := Node3D.new()
|
||
player.position = Vector3.ZERO
|
||
get_root().add_child(player)
|
||
|
||
# --- 小地图 ---
|
||
var mm: Node = Minimap.new()
|
||
get_root().add_child(mm)
|
||
mm.setup(fc, canvas, func() -> Node3D: return player)
|
||
_ck(mm._view != null, "minimap: view built")
|
||
fc.channel_changed.emit(7)
|
||
_ck(mm._ch_label.text == "CH 7", "minimap: channel label")
|
||
# _to_map: 北(−z 100m)应在圆心正上方附近;夹到半径内
|
||
var north: Vector2 = mm._to_map(Vector3(0, 0, -1000), Vector3.ZERO)
|
||
_ck(north.x == mm.RADIUS and north.y < mm.RADIUS, "minimap: north maps up, clamped")
|
||
var east: Vector2 = mm._to_map(Vector3(10, 0, 0), Vector3.ZERO)
|
||
_ck(east.x > mm.RADIUS and abs(east.y - mm.RADIUS) < 0.01, "minimap: east maps right")
|
||
var base_scale: float = mm.get_scale()
|
||
mm.set_scale(base_scale * 2.0)
|
||
var zoomed_east: Vector2 = mm._to_map(Vector3(10, 0, 0), Vector3.ZERO)
|
||
_ck(mm.get_scale() > base_scale and zoomed_east.x > east.x, "minimap: scale zooms world projection")
|
||
var wheel_scale: float = mm.get_scale()
|
||
var wheel := InputEventMouseButton.new()
|
||
wheel.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||
wheel.pressed = true
|
||
mm._on_gui_input(wheel)
|
||
_ck(mm.get_scale() < wheel_scale, "minimap: wheel down zooms out")
|
||
fc.entities = [{"vid": 11, "is_main": false, "ch_type": 0, "pos": Vector3(5, 0, 5)}]
|
||
fc.party = [{"vid": 11, "leader": true}]
|
||
fc.npc_marks = [{"type": 2, "name": "Gate", "pos": Vector3(3, 0, 3)}]
|
||
fc.markers = [{"id": 1, "name": "Quest", "pos": Vector3(-4, 0, -4)}]
|
||
fc.observers = [{"vid": 9001, "pos": Vector3(2, 0, -2)}]
|
||
fc.land_areas = [{"id": 42, "guild_id": 77, "x": -200, "y": -200, "width": 400, "height": 400}]
|
||
mm._view.queue_redraw()
|
||
await process_frame
|
||
_ck(mm._view.is_visible_in_tree(), "minimap: redraw scheduled, view live")
|
||
|
||
# --- 昼夜 ---
|
||
var sun := DirectionalLight3D.new()
|
||
get_root().add_child(sun)
|
||
var env := Environment.new()
|
||
env.background_mode = Environment.BG_COLOR
|
||
var wt: Node = WorldTime.new()
|
||
get_root().add_child(wt)
|
||
wt.setup(fc, sun, env)
|
||
# 正午:2025-01-01 12:00:00 UTC = 1735732800
|
||
fc._time = 1735732800
|
||
fc.time_changed.emit(1735732800)
|
||
_ck(abs(wt.day_fraction() - 0.5) < 0.01, "world_time: noon -> day_fraction ~0.5")
|
||
wt._process(0.016)
|
||
var noon_energy: float = sun.light_energy
|
||
_ck(noon_energy > 0.9, "world_time: noon sun bright (%.2f)" % noon_energy)
|
||
# 午夜
|
||
fc.time_changed.emit(1735732800 - 43200)
|
||
_ck(abs(wt.day_fraction()) < 0.01 or abs(wt.day_fraction() - 1.0) < 0.01,
|
||
"world_time: midnight -> fraction ~0")
|
||
wt._process(0.016)
|
||
_ck(sun.light_energy < 0.2, "world_time: midnight sun dark (%.3f)" % sun.light_energy)
|
||
|
||
# --- 天气 ---
|
||
var cam := Camera3D.new()
|
||
get_root().add_child(cam)
|
||
var w: Node3D = Weather.new()
|
||
get_root().add_child(w)
|
||
w.setup(cam)
|
||
_ck(w.kind() == "none" and not w._p.emitting, "weather: starts off")
|
||
w.set_weather("snow")
|
||
_ck(w._p.emitting and w.kind() == "snow", "weather: snow on")
|
||
var g_snow: Vector3 = w._p.process_material.gravity
|
||
w.set_weather("rain")
|
||
var g_rain: Vector3 = w._p.process_material.gravity
|
||
_ck(g_rain.y < g_snow.y, "weather: rain falls faster than snow")
|
||
w.set_weather("none")
|
||
_ck(not w._p.emitting, "weather: off again")
|
||
|
||
# --- 传送信号(同服)---
|
||
var got := []
|
||
fc.warp.connect(func(pos, same): got.append([pos, same]))
|
||
fc.warp.emit(Vector3(50, 0, -30), true)
|
||
_ck(got.size() == 1 and got[0][1] == true, "warp: same-server signal received")
|