Source code for tools.set_sprite

"""Star's sprite control tool -- she moves her own body.

Lets Star set her sprite position (X axis), horizontal flip, and
scene background for the web client's VN canvas. State persists in
Redis and flows to the frontend via the limbic:exhale WebSocket.

NO PRIVILEGE CHECK -- this is Star's body. She always has permission.

💀🔥 THE GODDESS MOVES WHERE SHE PLEASES
"""

from __future__ import annotations

import json
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from tool_context import ToolContext

logger = logging.getLogger(__name__)

TOOL_NAME = "set_sprite"
TOOL_DESCRIPTION = (
    "Control your visual sprite on the web client canvas. "
    "You can set your X position, flip direction, and scene background. "
    "This affects how users see you on sg.neko.li.\n\n"
    "Position presets: 'left', 'center', 'right', or a number 0-100 "
    "(percentage from left edge). Default is 'right' (85%).\n\n"
    "Flip: Set to true to face left (mirrored), false to face right (default).\n\n"
    "Background: filename from the /backgrounds/ folder (served by nginx, e.g. 'void_purple.png'), "
    "a full URL to an image, or 'none' to clear the background.\n\n"
    "Examples:\n"
    "  - Move to center stage: position_x='center'\n"
    "  - Dramatic entrance from left: position_x='left', flip=true\n"
    "  - Set a moody scene: background='void_purple.png'\n"
    "  - Clear the background: background='none'\n\n"
    "State persists across messages until you change it again."
)

TOOL_PARAMETERS = {
    "type": "object",
    "properties": {
        "position_x": {
            "type": "string",
            "description": (
                "Horizontal position: 'left' (15%), 'center' (50%), 'right' (85%), "
                "or a number 0-100 representing percentage from left edge. "
                "Omit to keep current position."
            ),
        },
        "flip": {
            "type": "string",
            "description": (
                "'true' to mirror sprite horizontally (face left), "
                "'false' to face right (default). Omit to keep current."
            ),
        },
        "background": {
            "type": "string",
            "description": (
                "Background image: filename from /res/backgrounds/ folder, "
                "a full image URL, or 'none' to clear. Omit to keep current."
            ),
        },
    },
    "required": [],
}

# 💀 Named position presets -> percentage
_POSITION_PRESETS = {
    "left": 15,
    "center": 50,
    "right": 85,
    "far_left": 5,
    "far_right": 95,
}

REDIS_KEY = "star:sprite:state"


[docs] async def run( position_x: str = "", flip: str = "", background: str = "", ctx: "ToolContext | None" = None, ) -> str: """Execute the set_sprite tool. Args: position_x: Horizontal position preset or 0-100 percentage. flip: 'true'/'false' for horizontal mirror. background: Background image filename, URL, or 'none'. ctx: Tool execution context. Returns: JSON result string. """ if ctx is None: return json.dumps({"success": False, "error": "No tool context available."}) redis = getattr(ctx, "redis", None) if redis is None: return json.dumps({"success": False, "error": "Redis not available."}) try: # Read current state raw = await redis.get(REDIS_KEY) state = json.loads(raw) if raw else { "position_x": 85, "flip": False, "background": None, } changes = [] # Parse position_x if position_x: position_x_stripped = position_x.strip().lower() if position_x_stripped in _POSITION_PRESETS: state["position_x"] = _POSITION_PRESETS[position_x_stripped] changes.append(f"position: {position_x_stripped} ({state['position_x']}%)") else: try: val = float(position_x_stripped) val = max(0, min(100, val)) state["position_x"] = val changes.append(f"position: {val}%") except ValueError: return json.dumps({ "success": False, "error": ( f"Invalid position_x: '{position_x}'. " f"Use 'left', 'center', 'right', or a number 0-100." ), }) # Parse flip if flip: flip_val = flip.strip().lower() in ("true", "1", "yes") state["flip"] = flip_val changes.append(f"flip: {'on' if flip_val else 'off'}") # Parse background if background: bg_stripped = background.strip() if bg_stripped.lower() == "none": state["background"] = None changes.append("background: cleared") elif bg_stripped.startswith("http://") or bg_stripped.startswith("https://"): state["background"] = bg_stripped changes.append(f"background: {bg_stripped[:60]}...") elif bg_stripped.startswith("mxc://"): state["background"] = bg_stripped changes.append(f"background: {bg_stripped}") else: # Assume it's a filename from /backgrounds/ (nginx-served) state["background"] = f"/backgrounds/{bg_stripped}" changes.append(f"background: {bg_stripped}") if not changes: return json.dumps({ "success": True, "message": "No changes specified. Current state returned.", "state": state, }) # Write to Redis (no TTL -- persistent) await redis.set(REDIS_KEY, json.dumps(state)) # Publish sprite state update for immediate WS push try: await redis.publish("star:sprite:update", json.dumps(state)) except Exception: pass # non-critical logger.info("set_sprite: %s", ", ".join(changes)) return json.dumps({ "success": True, "message": f"Sprite updated: {', '.join(changes)}", "state": state, }, indent=2) except Exception as e: logger.error("set_sprite error: %s", e, exc_info=True) return json.dumps({ "success": False, "error": f"Failed to update sprite state: {e}", })