diff --git a/src/scripts/api/README.md b/src/scripts/api/README.md index ca97e7e..eb81525 100644 --- a/src/scripts/api/README.md +++ b/src/scripts/api/README.md @@ -30,14 +30,44 @@ Returns the current scene graph with all UI elements. ```bash curl http://localhost:8765/scene + +# Omniscient mode (see all entities, ignoring FOV) +curl "http://localhost:8765/scene?omniscient=true" ``` -Response includes: +**Query Parameters:** +- `omniscient`: If `true`, show all entities regardless of FOV. Default is `false` (respect perspective). + +**Response includes:** - Scene name - Viewport dimensions - All UI elements with type, bounds, visibility, interactivity - Nested children - Type-specific properties (text for Caption, grid_size for Grid, etc.) +- `perspective_mode`: Either "respect_fov" or "omniscient" + +**Grid Perspective Support:** + +When a Grid has a `perspective` entity set (typically the player), the API respects +field-of-view by default. Only entities visible to the perspective entity are returned. + +```json +{ + "type": "Grid", + "entities": [...], // Only entities in FOV + "perspective": { + "enabled": true, + "entity_name": "player", + "entity_pos": {"x": 5, "y": 5}, + "fov_radius": 10, + "hidden_entities": 3, + "note": "Entities filtered by FOV - only showing what perspective entity can see" + } +} +``` + +Use `?omniscient=true` to bypass FOV filtering and see all entities (useful for +debugging or when partial information isn't needed). ### GET /affordances Returns only interactive elements with semantic labels. diff --git a/src/scripts/api/introspection.py b/src/scripts/api/introspection.py index 35fa86e..5860851 100644 --- a/src/scripts/api/introspection.py +++ b/src/scripts/api/introspection.py @@ -86,8 +86,17 @@ def serialize_entity(entity) -> Dict[str, Any]: return data -def serialize_grid(grid) -> Dict[str, Any]: - """Serialize a Grid element with its entities.""" +def serialize_grid(grid, respect_perspective: bool = True) -> Dict[str, Any]: + """Serialize a Grid element with its entities. + + Args: + grid: The Grid element to serialize + respect_perspective: If True and grid has a perspective entity, + only include entities visible to that entity's FOV + + Returns: + Dictionary representation of the grid + """ bounds = get_bounds(grid) # Get grid dimensions @@ -106,10 +115,27 @@ def serialize_grid(grid) -> Dict[str, Any]: center = getattr(grid, 'center', None) zoom = float(getattr(grid, 'zoom', 1.0)) - # Serialize entities + # Check for perspective (player POV) + perspective_entity = getattr(grid, 'perspective', None) + perspective_enabled = bool(getattr(grid, 'perspective_enabled', False)) + has_perspective = perspective_entity is not None and perspective_enabled + + # Serialize entities, optionally filtering by FOV entities = [] + hidden_count = 0 try: for entity in grid.entities: + # Check if entity is visible from perspective + if has_perspective and respect_perspective: + try: + entity_pos = (int(entity.grid_x), int(entity.grid_y)) + in_fov = grid.is_in_fov(entity_pos) + if not in_fov: + hidden_count += 1 + continue # Skip entities not in FOV + except Exception: + pass # If FOV check fails, include the entity + entities.append(serialize_entity(entity)) except Exception: pass @@ -129,23 +155,42 @@ def serialize_grid(grid) -> Dict[str, Any]: "has_cell_enter": getattr(grid, 'on_cell_enter', None) is not None, } + # Add perspective info + if has_perspective: + data["perspective"] = { + "enabled": True, + "entity_name": getattr(perspective_entity, 'name', None) or "", + "entity_pos": { + "x": float(getattr(perspective_entity, 'grid_x', 0)), + "y": float(getattr(perspective_entity, 'grid_y', 0)) + }, + "fov_radius": int(getattr(grid, 'fov_radius', 0)), + "hidden_entities": hidden_count, + "note": "Entities filtered by FOV - only showing what perspective entity can see" + } + else: + data["perspective"] = { + "enabled": False, + "note": "No perspective set - showing all entities (omniscient view)" + } + # Add cell size estimate if texture available texture = getattr(grid, 'texture', None) if texture: - # Texture dimensions divided by sprite count would give cell size - # but this is an approximation data["has_texture"] = True return data -def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str, Any]: +def serialize_element(element, depth: int = 0, max_depth: int = 10, + respect_perspective: bool = True) -> Dict[str, Any]: """Serialize a UI element to a dictionary. Args: element: The UI element to serialize depth: Current recursion depth max_depth: Maximum recursion depth for children + respect_perspective: If True, filter grid entities by perspective FOV Returns: Dictionary representation of the element @@ -155,7 +200,7 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str, # Special handling for Grid if element_type == "Grid": - return serialize_grid(element) + return serialize_grid(element, respect_perspective=respect_perspective) data = { "type": element_type, @@ -195,7 +240,8 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str, children = [] try: for child in element.children: - children.append(serialize_element(child, depth + 1, max_depth)) + children.append(serialize_element(child, depth + 1, max_depth, + respect_perspective)) except Exception: pass data["children"] = children @@ -204,9 +250,13 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str, return data -def serialize_scene() -> Dict[str, Any]: +def serialize_scene(respect_perspective: bool = True) -> Dict[str, Any]: """Serialize the entire current scene graph. + Args: + respect_perspective: If True, filter grid entities by perspective FOV. + If False, show all entities (omniscient view). + Returns: Dictionary with scene name, viewport info, and all elements """ @@ -224,7 +274,8 @@ def serialize_scene() -> Dict[str, Any]: try: if scene: for element in scene.children: - elements.append(serialize_element(element)) + elements.append(serialize_element(element, + respect_perspective=respect_perspective)) except Exception as e: pass @@ -233,7 +284,8 @@ def serialize_scene() -> Dict[str, Any]: "timestamp": time.time(), "viewport": {"width": width, "height": height}, "element_count": len(elements), - "elements": elements + "elements": elements, + "perspective_mode": "respect_fov" if respect_perspective else "omniscient" } diff --git a/src/scripts/api/server.py b/src/scripts/api/server.py index 4a9bb51..1ff2524 100644 --- a/src/scripts/api/server.py +++ b/src/scripts/api/server.py @@ -92,7 +92,7 @@ class GameAPIHandler(BaseHTTPRequestHandler): query = parse_qs(parsed.query) if path == '/scene': - self.handle_scene() + self.handle_scene(query) elif path == '/affordances': self.handle_affordances() elif path == '/screenshot': @@ -145,16 +145,26 @@ class GameAPIHandler(BaseHTTPRequestHandler): "timestamp": time.time() }) - def handle_scene(self) -> None: - """Return the current scene graph with semantic annotations.""" + def handle_scene(self, query: Dict) -> None: + """Return the current scene graph with semantic annotations. + + Query parameters: + omniscient: If 'true', show all entities regardless of FOV. + Default is to respect perspective (show only what + the perspective entity can see). + """ + # Check if client wants omniscient (all-seeing) view + omniscient = query.get('omniscient', ['false'])[0].lower() == 'true' + respect_perspective = not omniscient + try: # Try to use lock for thread safety, but fall back if not available try: with mcrfpy.lock(): - scene_data = serialize_scene() + scene_data = serialize_scene(respect_perspective=respect_perspective) except (RuntimeError, AttributeError): # Lock not available (e.g., main thread or headless mode issue) - scene_data = serialize_scene() + scene_data = serialize_scene(respect_perspective=respect_perspective) self.send_json(scene_data) except Exception as e: self.send_error_json(f"Scene introspection failed: {str(e)}", 500)