diff --git a/godot-addon/addons/godotiq/godotiq_server.gd b/godot-addon/addons/godotiq/godotiq_server.gd
index 8e16633..2d6842e 100644
--- a/godot-addon/addons/godotiq/godotiq_server.gd
+++ b/godot-addon/addons/godotiq/godotiq_server.gd
@@ -4,7 +4,7 @@ extends Node
 ## dispatches requests to editor handlers or forwards to the running game.
 
 const DEFAULT_PORT := 6007
-const ADDON_VERSION := "2.0.0"
+const ADDON_VERSION := "0.1.0"
 const SCREENSHOT_TIMEOUT_MS := 30000
 const PERF_TIMEOUT_MS := 5000
 const INPUT_TIMEOUT_MS := 65000
@@ -24,6 +24,7 @@ var _next_peer_id: int = 1
 var _port: int = DEFAULT_PORT
 var debugger  # untyped — set by godotiq_plugin.gd
 var undo_redo  # untyped — set by godotiq_plugin.gd (EditorUndoRedoManager)
+var status_label: Label  # Bottom panel status label — set by godotiq_plugin.gd
 var _godotiq_action_history: Array = []
 const MAX_HISTORY_SIZE: int = 50
 var _recent_errors: Array = []
@@ -34,6 +35,7 @@ const MAX_SCRIPT_ERRORS: int = 50
 var _error_logger  # Variant — null if Logger unavailable
 var _has_logger: bool = false
 var _checking_errors: bool = false
+var _update_checked: bool = false
 
 
 func _clear_script_errors() -> void:
@@ -62,8 +64,11 @@ func _ready() -> void:
 	var err := _tcp_server.listen(_port, "127.0.0.1")
 	if err == OK:
 		print("GodotIQ: WebSocket server listening on 127.0.0.1:%d" % _port)
+		_update_status_label()
 	else:
 		push_error("GodotIQ: Failed to start WebSocket server on port %d (error %d)" % [_port, err])
+		if status_label:
+			status_label.text = "Server: Failed to start (port %d)" % _port
 	# Logger registration (Godot 4.5+ only)
 	if ClassDB.class_exists("Logger"):
 		var logger_script = load("res://addons/godotiq/godotiq_logger.gd")
@@ -74,12 +79,63 @@ func _ready() -> void:
 			print("GodotIQ: Logger registered (Godot 4.5+)")
 		else:
 			push_warning("GodotIQ: Logger class exists but godotiq_logger.gd failed to load")
+	# One-time update check against PyPI
+	if not _update_checked:
+		_update_checked = true
+		_check_for_update()
+
+
+func _check_for_update() -> void:
+	var http := HTTPRequest.new()
+	http.timeout = 10.0
+	add_child(http)
+	http.request_completed.connect(_on_update_check_completed.bind(http))
+	var err := http.request("https://pypi.org/pypi/godotiq/json")
+	if err != OK:
+		http.queue_free()
+
+
+func _on_update_check_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, http: HTTPRequest) -> void:
+	http.queue_free()
+	if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
+		return
+	var parsed = JSON.parse_string(body.get_string_from_utf8())
+	if parsed == null or not parsed.has("info") or not parsed["info"].has("version"):
+		return
+	var remote_version: String = parsed["info"]["version"]
+	if _is_newer_version(remote_version, ADDON_VERSION):
+		print("GodotIQ: Update available — v%s (current: v%s)" % [remote_version, ADDON_VERSION])
+		if status_label:
+			status_label.text = "GodotIQ v%s — Update available: v%s (pip install --upgrade godotiq && godotiq install-addon .)" % [ADDON_VERSION, remote_version]
+
+
+func _is_newer_version(remote: String, local: String) -> bool:
+	var r := remote.split(".")
+	var l := local.split(".")
+	var max_len := max(r.size(), l.size())
+	for i in range(max_len):
+		var rv: int = int(r[i]) if i < r.size() else 0
+		var lv: int = int(l[i]) if i < l.size() else 0
+		if rv > lv:
+			return true
+		if rv < lv:
+			return false
+	return false
 
 
 func get_port() -> int:
 	return _port
 
 
+func _update_status_label() -> void:
+	if status_label == null:
+		return
+	if _peers.size() > 0:
+		status_label.text = "Server: Connected (port %d)" % _port
+	else:
+		status_label.text = "Server: Listening (port %d)" % _port
+
+
 func _load_port_from_config() -> int:
 	if not FileAccess.file_exists("res://.godotiq.json"):
 		return DEFAULT_PORT
@@ -114,6 +170,7 @@ func _process(_delta: float) -> void:
 			_next_peer_id += 1
 			_peers[peer_id] = ws
 			_peer_tcp[peer_id] = tcp
+			_update_status_label()
 		else:
 			push_warning("GodotIQ: Failed to accept WebSocket stream (error %d)" % err)
 
@@ -136,6 +193,8 @@ func _process(_delta: float) -> void:
 	for peer_id in to_remove:
 		_peers.erase(peer_id)
 		_peer_tcp.erase(peer_id)
+	if to_remove.size() > 0:
+		_update_status_label()
 
 	# 4. Process deferred editor screenshot
 	if _pending_screenshot != null:
@@ -158,6 +217,7 @@ func _process(_delta: float) -> void:
 			"timeout_at": Time.get_ticks_msec() + int(s.get("timeout", 15.0) * 1000),
 			"scene": s.get("scene_path", ""),
 			"main_scene_set": s.get("main_scene_set", false),
+			"script_warnings": s.get("script_warnings", []),
 		}
 
 	# 4b. Poll for scene play confirmation
@@ -169,6 +229,9 @@ func _process(_delta: float) -> void:
 			var resp := {"action": "play", "success": true, "scene": r.get("scene", ""), "waited_seconds": waited}
 			if r.get("main_scene_set", false):
 				resp["main_scene_set"] = true
+			var sw = r.get("script_warnings", [])
+			if sw.size() > 0:
+				resp["script_warnings"] = sw
 			send_response(r["peer_id"], r["id"], resp)
 		elif Time.get_ticks_msec() > r["timeout_at"]:
 			_pending_run = null
@@ -308,7 +371,7 @@ func _handle_reload_script(peer_id: int, id: String, params: Dictionary) -> void
 	if script == null or not (script is GDScript):
 		send_response(peer_id, id, {"reloaded": false, "error": "Failed to load script: %s" % path})
 		return
-	var err := script.reload()
+	var err: int = script.reload()
 	if err != OK:
 		send_response(peer_id, id, {"reloaded": false, "error": "Reload failed with code %d" % err})
 		return
@@ -376,8 +439,8 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 			resolved_path = result["path"]
 
 	# If using current scene, resolve from editor
+	var edited_root = EditorInterface.get_edited_scene_root()
 	if is_current:
-		var edited_root := EditorInterface.get_edited_scene_root()
 		if edited_root:
 			resolved_path = edited_root.scene_file_path
 		else:
@@ -387,7 +450,6 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 	# Step 3: Pre-flight script validation (PRESERVED)
 	var script_paths: Array[String] = []
 	if is_current:
-		var edited_root := EditorInterface.get_edited_scene_root()
 		if edited_root:
 			script_paths.append_array(_get_scene_script_paths(edited_root))
 	else:
@@ -399,13 +461,7 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 		if not seen.has(p):
 			seen[p] = true
 			unique_paths.append(p)
-	var errors := _check_scripts_valid(unique_paths)
-	if errors.size() > 0:
-		_send_error(
-			peer_id, id, "SCRIPT_ERRORS",
-			"Cannot start game: %d script(s) have errors. Use godotiq_check_errors for details." % errors.size()
-		)
-		return
+	var script_warnings := _check_scripts_valid(unique_paths)
 
 	# Step 4: Auto-set main scene if empty
 	var main_scene_set: bool = false
@@ -419,7 +475,7 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 	var timeout: float = float(params.get("timeout", 15.0))
 
 	# Step 6: Open scene if needed, then play
-	var edited_root := EditorInterface.get_edited_scene_root()
+	edited_root = EditorInterface.get_edited_scene_root()
 	var current_scene_path: String = edited_root.scene_file_path if edited_root else ""
 
 	if not is_current and resolved_path != current_scene_path:
@@ -431,6 +487,7 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 			"scene_path": resolved_path,
 			"timeout": timeout,
 			"main_scene_set": main_scene_set,
+			"script_warnings": script_warnings,
 		}
 	else:
 		# Scene already open — play immediately
@@ -442,6 +499,7 @@ func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
 			"timeout_at": Time.get_ticks_msec() + int(timeout * 1000),
 			"scene": resolved_path,
 			"main_scene_set": main_scene_set,
+			"script_warnings": script_warnings,
 		}
 
 
@@ -487,8 +545,10 @@ func _check_scripts_valid(script_paths: Array[String]) -> Array[Dictionary]:
 		var res = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
 		if res == null:
 			errors.append({"file": path, "error": "Failed to load script"})
-		elif res is GDScript and not res.can_instantiate():
-			errors.append({"file": path, "error": "Script has compilation errors"})
+		elif res is GDScript:
+			var err: int = res.reload()
+			if err != OK:
+				errors.append({"file": path, "error": "Script reload failed (error %d)" % err})
 	return errors
 
 
@@ -1477,19 +1537,28 @@ func _handle_build_scene(peer_id: int, id: String, params: Dictionary) -> void:
 
 
 func _expand_build_pattern(params: Dictionary) -> Array:
+	var offset: Array = params.get("offset", [0, 0, 0])
 	if params.has("grid"):
-		return _expand_build_grid(params["grid"])
+		return _expand_build_grid(params["grid"], offset)
 	if params.has("line"):
-		return _expand_build_line(params["line"])
+		return _expand_build_line(params["line"], offset)
 	if params.has("scatter"):
 		var scatter: Dictionary = params["scatter"]
-		return scatter.get("items", [])
+		var items: Array = scatter.get("items", [])
+		var off_x: float = float(offset[0])
+		var off_y: float = float(offset[1])
+		var off_z: float = float(offset[2])
+		for item in items:
+			if item is Dictionary and item.has("position"):
+				var pos: Array = item["position"]
+				item["position"] = [float(pos[0]) + off_x, float(pos[1]) + off_y, float(pos[2]) + off_z]
+		return items
 	if params.has("nodes"):
 		return params["nodes"]
 	return []
 
 
-func _expand_build_grid(grid: Dictionary) -> Array:
+func _expand_build_grid(grid: Dictionary, offset: Array = [0, 0, 0]) -> Array:
 	var scene_path: String = str(grid.get("scene", ""))
 	var prefix: String = str(grid.get("prefix", "Node"))
 	var rows: int = int(grid.get("rows", 1))
@@ -1503,6 +1572,9 @@ func _expand_build_grid(grid: Dictionary) -> Array:
 	var ox: float = float(origin[0]) if origin.size() > 0 else 0.0
 	var oy: float = float(origin[1]) if origin.size() > 1 else 0.0
 	var oz: float = float(origin[2]) if origin.size() > 2 else 0.0
+	var off_x: float = float(offset[0])
+	var off_y: float = float(offset[1])
+	var off_z: float = float(offset[2])
 
 	var result: Array = []
 	for row in range(rows):
@@ -1527,21 +1599,27 @@ func _expand_build_grid(grid: Dictionary) -> Array:
 
 			if not spec.has("position"):
 				if axis == "xy":
-					spec["position"] = [ox + col * spacing, oy + row * spacing, oz]
+					spec["position"] = [ox + col * spacing + off_x, oy + row * spacing + off_y, oz + off_z]
 				else:  # "xz" default
-					spec["position"] = [ox + col * spacing, oy, oz + row * spacing]
+					spec["position"] = [ox + col * spacing + off_x, oy + off_y, oz + row * spacing + off_z]
+			else:
+				var pos: Array = spec["position"]
+				spec["position"] = [float(pos[0]) + off_x, float(pos[1]) + off_y, float(pos[2]) + off_z]
 
 			result.append(spec)
 	return result
 
 
-func _expand_build_line(line: Dictionary) -> Array:
+func _expand_build_line(line: Dictionary, offset: Array = [0, 0, 0]) -> Array:
 	var scene_path: String = str(line.get("scene", ""))
 	var prefix: String = str(line.get("prefix", "Node"))
 	var points: Array = line.get("points", [])
 	var spacing: float = float(line.get("spacing", 1.0))
 	var align_to_path: bool = line.get("align_to_path", false)
 	var type_name: String = str(line.get("type", ""))
+	var off_x: float = float(offset[0])
+	var off_y: float = float(offset[1])
+	var off_z: float = float(offset[2])
 
 	if points.size() < 2 or spacing <= 0:
 		return []
@@ -1551,7 +1629,7 @@ func _expand_build_line(line: Dictionary) -> Array:
 
 	# Place first node at start point with rotation from first segment
 	var first_pt: Array = points[0]
-	var first_spec: Dictionary = {"name": "%s_%d" % [prefix, index], "position": first_pt}
+	var first_spec: Dictionary = {"name": "%s_%d" % [prefix, index], "position": [float(first_pt[0]) + off_x, float(first_pt[1]) + off_y, float(first_pt[2]) + off_z]}
 	if scene_path != "":
 		first_spec["scene"] = scene_path
 	if type_name != "":
@@ -1582,7 +1660,7 @@ func _expand_build_line(line: Dictionary) -> Array:
 			var pos := start + seg_dir * dist_along
 			var spec: Dictionary = {
 				"name": "%s_%d" % [prefix, index],
-				"position": [pos.x, pos.y, pos.z],
+				"position": [pos.x + off_x, pos.y + off_y, pos.z + off_z],
 			}
 			if scene_path != "":
 				spec["scene"] = scene_path
@@ -1611,7 +1689,7 @@ func _expand_build_line(line: Dictionary) -> Array:
 	if place_endpoint:
 		var end_spec: Dictionary = {
 			"name": "%s_%d" % [prefix, index],
-			"position": [final_pos.x, final_pos.y, final_pos.z],
+			"position": [final_pos.x + off_x, final_pos.y + off_y, final_pos.z + off_z],
 		}
 		if scene_path != "":
 			end_spec["scene"] = scene_path
diff --git a/src/godotiq/tools/bridge/__init__.py b/src/godotiq/tools/bridge/__init__.py
index 028a329..eb5e3d2 100644
--- a/src/godotiq/tools/bridge/__init__.py
+++ b/src/godotiq/tools/bridge/__init__.py
@@ -740,6 +740,7 @@ async def godotiq_build_scene(
     line: dict | None = None,
     scatter: dict | None = None,
     nodes: list[dict] | None = None,
+    offset: list[float] | None = None,
     ctx: Context = None,
 ) -> dict:
     """Create multiple nodes in a scene using high-level patterns. One call = many nodes with Ctrl+Z undo. Use INSTEAD of repeated node_ops add_child calls.
@@ -758,11 +759,12 @@ async def godotiq_build_scene(
         line: Line pattern config.
         scatter: Scatter placement config.
         nodes: Explicit node definitions.
+        offset: Shift entire pattern by [x, y, z] (default [0, 0, 0]). Applies to all modes.
     """
     try:
         result = await _build_scene(
             grid=grid, line=line, scatter=scatter, nodes=nodes,
-            parent=parent, target_scene=target_scene,
+            parent=parent, target_scene=target_scene, offset=offset,
         )
         if "error" in result:
             return result
diff --git a/src/godotiq/tools/bridge/build_scene.py b/src/godotiq/tools/bridge/build_scene.py
index 8df1b21..49303ab 100644
--- a/src/godotiq/tools/bridge/build_scene.py
+++ b/src/godotiq/tools/bridge/build_scene.py
@@ -14,11 +14,21 @@ def validate_build_params(
     line: dict | None,
     scatter: dict | None,
     nodes: list[dict] | None,
+    offset: list[float] | None = None,
 ) -> tuple[dict | None, dict | None]:
     """Validate build_scene parameters.
 
     Returns (params_dict, None) on success, or (None, error_dict) on failure.
     """
+    # Validate offset first
+    if offset is not None:
+        if not isinstance(offset, (list, tuple)) or len(offset) != 3:
+            return None, {"error": "offset must be a list of exactly 3 numbers [x, y, z]"}
+        if not all(isinstance(el, (int, float)) for el in offset):
+            return None, {"error": "offset must be a list of exactly 3 numbers [x, y, z]"}
+
+    resolved_offset = list(offset) if offset is not None else [0, 0, 0]
+
     modes = [(k, v) for k, v in [("grid", grid), ("line", line), ("scatter", scatter), ("nodes", nodes)] if v is not None]
 
     if len(modes) == 0:
@@ -30,13 +40,19 @@ def validate_build_params(
     mode_name, mode_value = modes[0]
 
     if mode_name == "grid":
-        return _validate_grid(mode_value)
+        params, error = _validate_grid(mode_value)
     elif mode_name == "line":
-        return _validate_line(mode_value)
+        params, error = _validate_line(mode_value)
     elif mode_name == "scatter":
-        return _validate_scatter(mode_value)
+        params, error = _validate_scatter(mode_value)
     else:
-        return _validate_nodes(mode_value)
+        params, error = _validate_nodes(mode_value)
+
+    if error is not None:
+        return None, error
+
+    params["offset"] = resolved_offset
+    return params, None
 
 
 def _validate_grid(grid: dict) -> tuple[dict | None, dict | None]:
@@ -164,6 +180,7 @@ async def build_scene(
     nodes: list[dict] | None = None,
     parent: str = "",
     target_scene: str = "",
+    offset: list[float] | None = None,
 ) -> dict:
     """Send a build_scene request to the Godot addon.
 
@@ -176,11 +193,12 @@ async def build_scene(
         nodes: Explicit node list (type, name, position, etc.).
         parent: Parent node path in the scene.
         target_scene: Target .tscn path for context.
+        offset: Shift entire pattern by [x, y, z] (default [0, 0, 0]).
 
     Returns:
         Bridge response dict, or error dict if validation fails.
     """
-    params, error = validate_build_params(grid, line, scatter, nodes)
+    params, error = validate_build_params(grid, line, scatter, nodes, offset=offset)
     if error is not None:
         return error
 
diff --git a/tests/test_tools/test_build_scene.py b/tests/test_tools/test_build_scene.py
index 98031b5..f47753c 100644
--- a/tests/test_tools/test_build_scene.py
+++ b/tests/test_tools/test_build_scene.py
@@ -246,6 +246,77 @@ class TestBuildSceneTimeout:
         assert TOOL_TIMEOUTS["build_scene"] == 30.0
 
 
+class TestOffsetValidation:
+    """Offset parameter validation for build_scene."""
+
+    def test_valid_offset_accepted(self) -> None:
+        """validate_build_params accepts grid with valid offset [10, 0, 5]."""
+        params, error = validate_build_params(
+            grid={"rows": 2, "cols": 2}, line=None, scatter=None, nodes=None,
+            offset=[10, 0, 5],
+        )
+        assert error is None
+        assert params is not None
+        assert params["offset"] == [10, 0, 5]
+
+    def test_no_offset_backward_compat(self) -> None:
+        """validate_build_params accepts grid with no offset (backward compat)."""
+        params, error = validate_build_params(
+            grid={"rows": 2, "cols": 2}, line=None, scatter=None, nodes=None,
+        )
+        assert error is None
+        assert params is not None
+        assert params["offset"] == [0, 0, 0]
+
+    def test_offset_wrong_length_rejected(self) -> None:
+        """validate_build_params rejects offset with wrong length [1, 2]."""
+        params, error = validate_build_params(
+            grid={"rows": 2, "cols": 2}, line=None, scatter=None, nodes=None,
+            offset=[1, 2],
+        )
+        assert params is None
+        assert "3" in error["error"]
+
+    def test_offset_non_numeric_rejected(self) -> None:
+        """validate_build_params rejects offset with non-numeric values."""
+        params, error = validate_build_params(
+            grid={"rows": 2, "cols": 2}, line=None, scatter=None, nodes=None,
+            offset=["a", 0, 0],
+        )
+        assert params is None
+        assert "error" in error
+
+    @pytest.mark.asyncio
+    async def test_offset_passed_to_bridge(self) -> None:
+        """build_scene passes offset through to bridge request params."""
+        from unittest.mock import AsyncMock, patch, MagicMock
+
+        mock_bridge = MagicMock()
+        mock_bridge.request = AsyncMock(return_value={"ok": True, "created": 4})
+
+        with patch("godotiq.tools.bridge.build_scene.get_bridge", return_value=mock_bridge):
+            from godotiq.tools.bridge.build_scene import build_scene
+            await build_scene(grid={"rows": 2, "cols": 2}, offset=[10, 0, 5])
+
+        call_kwargs = mock_bridge.request.call_args
+        assert call_kwargs[1]["params"]["offset"] == [10, 0, 5]
+
+    @pytest.mark.asyncio
+    async def test_offset_defaults_to_zero(self) -> None:
+        """build_scene defaults offset to [0, 0, 0] when not provided."""
+        from unittest.mock import AsyncMock, patch, MagicMock
+
+        mock_bridge = MagicMock()
+        mock_bridge.request = AsyncMock(return_value={"ok": True, "created": 4})
+
+        with patch("godotiq.tools.bridge.build_scene.get_bridge", return_value=mock_bridge):
+            from godotiq.tools.bridge.build_scene import build_scene
+            await build_scene(grid={"rows": 2, "cols": 2})
+
+        call_kwargs = mock_bridge.request.call_args
+        assert call_kwargs[1]["params"]["offset"] == [0, 0, 0]
+
+
 class TestGridConnectivityValidation:
     """Tests for validate_grid_connectivity() function."""
 
