extends Node ## NS-16: POST MoveCommand then GET PositionState; emits authoritative world position for the ## avatar. ## (No `class_name` so headless/CI can load the project before `.godot` import exists.) signal authoritative_position_received(world: Vector3) enum Phase { BOOT_GET, POST_MOVE, VERIFY_GET } @export var base_url: String = "http://127.0.0.1:5253" @export var dev_player_id: String = "dev-local-1" var _http: HTTPRequest var _busy: bool = false var _phase: Phase = Phase.BOOT_GET func _ready() -> void: _http = HTTPRequest.new() add_child(_http) _http.request_completed.connect(_on_request_completed) func sync_from_server() -> void: if _busy: return _busy = true _phase = Phase.BOOT_GET _request_get() func submit_move_target(world: Vector3) -> void: if _busy: return _busy = true _phase = Phase.POST_MOVE var url := "%s/game/players/%s/move" % [_base_root(), _player_path_segment()] var payload := { "schemaVersion": 1, "target": {"x": world.x, "y": world.y, "z": world.z}, } var body := JSON.stringify(payload) var headers := PackedStringArray(["Content-Type: application/json"]) var err: Error = _http.request(url, headers, HTTPClient.METHOD_POST, body) if err != OK: push_warning("PositionAuthorityClient: POST failed to start (%s)" % err) _busy = false func _base_root() -> String: return base_url.strip_edges().rstrip("/") func _player_path_segment() -> String: # ASCII-safe ids (e.g. dev-local-1); use percent-encoding if ids gain reserved URL characters. return dev_player_id.strip_edges() func _request_get() -> void: var url := "%s/game/players/%s/position" % [_base_root(), _player_path_segment()] var err: Error = _http.request(url) if err != OK: push_warning("PositionAuthorityClient: GET failed to start (%s)" % err) _busy = false func _on_request_completed( _result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray ) -> void: if _result != HTTPRequest.RESULT_SUCCESS: _busy = false return var text := body.get_string_from_utf8() match _phase: Phase.BOOT_GET: _busy = false if response_code == 200: _emit_position_from_response(text) Phase.POST_MOVE: if response_code != 200: _busy = false return _phase = Phase.VERIFY_GET _request_get() Phase.VERIFY_GET: _busy = false if response_code == 200: _emit_position_from_response(text) func _emit_position_from_response(json_text: String) -> void: var parsed: Variant = JSON.parse_string(json_text) if parsed == null: return if not parsed is Dictionary: return var data: Dictionary = parsed var pos_variant: Variant = data.get("position", null) if not pos_variant is Dictionary: return var pos: Dictionary = pos_variant var x: float = float(pos.get("x", 0.0)) var y: float = float(pos.get("y", 0.0)) var z: float = float(pos.get("z", 0.0)) authoritative_position_received.emit(Vector3(x, y, z))