# 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_reset() 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}] # ClientVS22's CPythonMiniMap interpolates observer moves for 1 second. fc.observer_event.emit(mm.OBSERVER_ADD, 9001, Vector2(0, 0)) var observer_start: Vector3 = mm._observer_position(9001, Vector3.ZERO) _ck(observer_start.distance_to(Vector3.ZERO) < 0.01, "minimap: observer add seeds current position") fc.observer_event.emit(mm.OBSERVER_MOVE, 9001, Vector2(10, 4)) var observer_immediate: Vector3 = mm._observer_position(9001, Vector3.ZERO) _ck(observer_immediate.distance_to(Vector3.ZERO) < 0.1, "minimap: observer move starts at current position") await create_timer(1.05).timeout var observer_final: Vector3 = mm._observer_position(9001, Vector3.ZERO) _ck(observer_final.distance_to(Vector3(10, 0, 4)) < 0.1, "minimap: observer move reaches destination") fc.observer_event.emit(mm.OBSERVER_REMOVE, 9001, Vector2.ZERO) _ck(not mm._observer_tracks.has(9001), "minimap: observer remove clears track") fc.observer_event.emit(mm.OBSERVER_ADD, 9001, Vector2(2, -2)) fc.world_reset.emit() _ck(mm._observer_tracks.is_empty(), "minimap: world reset clears observer tracks") mm._view.queue_redraw() await process_frame _ck(mm._view.is_visible_in_tree(), "minimap: redraw scheduled, view live") # --- 任务信标:圆内画星,越出半径贴圆周画方向箭头(§5.2 世界箭头 / 屏幕边缘指示)--- var rim: float = mm.RADIUS - mm.SIGNAL_RIM_INSET var near_geo: Dictionary = mm.signal_marker_geometry(Vector2(10, 0), rim) _ck(not near_geo["clamped"] and near_geo["offset"] == Vector2(10, 0), "minimap signal: inside rim keeps true offset") var far_geo: Dictionary = mm.signal_marker_geometry(Vector2(400, 0), rim) _ck(far_geo["clamped"] and abs(far_geo["offset"].length() - rim) < 0.01, "minimap signal: outside rim clamps to circle") _ck(abs(far_geo["angle"]) < 0.001 and abs(far_geo["offset"].x - rim) < 0.01, "minimap signal: clamp keeps bearing to target (east)") var diag_geo: Dictionary = mm.signal_marker_geometry(Vector2(-300, -300), rim) _ck(diag_geo["clamped"] and abs(diag_geo["angle"] - atan2(-300.0, -300.0)) < 0.001 and abs(diag_geo["offset"].length() - rim) < 0.01, "minimap signal: diagonal clamp preserves atan2 bearing") var edge_geo: Dictionary = mm.signal_marker_geometry(Vector2(rim, 0), rim) _ck(edge_geo["clamped"], "minimap signal: exactly on rim counts as clamped (>=)") mm.add_signal_point(120000.0, 90000.0) _ck(mm.signal_point_count() == 1, "minimap signal: add_signal_point stores waypoint") mm._view.queue_redraw() await process_frame mm.clear_signal_points() _ck(mm.signal_point_count() == 0, "minimap signal: clear_signal_points empties waypoints") # --- 昼夜 --- 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")