extends Node ## NEO-29: client bridge for server-owned `HotbarLoadout` v1 fetch/update. signal loadout_changed(loadout: Dictionary) signal loadout_update_denied(reason_code: String, loadout: Dictionary) @export var base_url: String = "http://127.0.0.1:5253" @export var dev_player_id: String = "dev-local-1" @export var injected_http: Node = null var _http: Node var _busy: bool = false var _last_loadout: Dictionary = {} var _hotbar_state: Node = null func _ready() -> void: if injected_http != null: _http = injected_http else: _http = HTTPRequest.new() add_child(_http) if _http is HTTPRequest: (_http as HTTPRequest).timeout = 30.0 @warning_ignore("unsafe_method_access") _http.request_completed.connect(_on_request_completed) func set_hotbar_state(state_node: Node) -> void: _hotbar_state = state_node func request_sync_from_server() -> void: if _busy: return _busy = true var url := "%s/game/players/%s/hotbar-loadout" % [_base_root(), _player_path_segment()] var err: Error = _http.request(url) if err != OK: push_warning("HotbarLoadoutClient: GET failed to start (%s)" % err) _busy = false func request_bind_slot(slot_index: int, ability_id: String) -> void: if _busy: return var payload: Dictionary = { "schemaVersion": 1, "slots": [{"slotIndex": slot_index, "abilityId": ability_id.strip_edges()}], } _post_update(payload) func cached_loadout() -> Dictionary: return _last_loadout.duplicate(true) func _post_update(payload: Dictionary) -> void: _busy = true var url := "%s/game/players/%s/hotbar-loadout" % [_base_root(), _player_path_segment()] var headers := PackedStringArray(["Content-Type: application/json"]) var err: Error = _http.request(url, headers, HTTPClient.METHOD_POST, JSON.stringify(payload)) if err != OK: push_warning("HotbarLoadoutClient: POST failed to start (%s)" % err) _busy = false func _base_root() -> String: return base_url.strip_edges().rstrip("/") func _player_path_segment() -> String: return dev_player_id.strip_edges() func _on_request_completed( result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray ) -> void: _busy = false if result != HTTPRequest.RESULT_SUCCESS: push_warning("HotbarLoadoutClient: HTTP failed (result=%s)" % result) return var text := body.get_string_from_utf8() var parsed: Variant = JSON.parse_string(text) if not parsed is Dictionary: if response_code != 200: push_warning("HotbarLoadoutClient: HTTP %s non-JSON body" % response_code) return var data: Dictionary = parsed var loadout: Dictionary = data if data.has("loadout") and data["loadout"] is Dictionary: loadout = data["loadout"] if loadout.is_empty() or not loadout.has("slots"): return _last_loadout = loadout.duplicate(true) _apply_state(loadout) loadout_changed.emit(_last_loadout.duplicate(true)) if data.has("updated") and not bool(data.get("updated", false)): var reason_variant: Variant = data.get("reasonCode", "") var reason: String = reason_variant as String if reason_variant is String else "" if not reason.is_empty(): loadout_update_denied.emit(reason, _last_loadout.duplicate(true)) func _apply_state(loadout: Dictionary) -> void: if _hotbar_state == null or not is_instance_valid(_hotbar_state): return if not _hotbar_state.has_method("apply_slots"): return var slots_variant: Variant = loadout.get("slots", []) if slots_variant is Array: _hotbar_state.call("apply_slots", slots_variant as Array)