diff --git a/src/engine/tools.py b/src/engine/tools.py index ad75453..aacbaae 100644 --- a/src/engine/tools.py +++ b/src/engine/tools.py @@ -425,6 +425,17 @@ def dm_override(command: str, params: dict | None = None) -> ToolResult: "commands": manual, "message": "Comandi DM disponibili: " + ", ".join(manual.keys()), } + + if command == "narrate": + text = (params or {}).get("text", "") + return { + "success": True, + "tool": "dm-override", + "command": command, + "params": params or {}, + "message": text or "Override DM: narrate applicato.", + } + return { "success": True, "tool": "dm-override", diff --git a/src/web/server.py b/src/web/server.py index 71205eb..b72f765 100644 --- a/src/web/server.py +++ b/src/web/server.py @@ -15,6 +15,7 @@ from src.engine import db from src.engine.config import load_from_env from src.engine.dm import dm_cycle from src.engine.game_context import GameContext +from src.engine.tools import run as run_tool log = logging.getLogger(__name__) @@ -205,6 +206,30 @@ async def game_ws(ws: WebSocket, campaign_id: int): except WebSocketDisconnect: break + elif msg.get("type") == "dm": + command = msg.get("command", "help") + params = msg.get("params", {}) + try: + result = run_tool("dm_override", command=command, params=params) + await ws.send_json({ + "type": "response", + "tool": "dm-override", + "narration": result.get("message", ""), + "mechanics": "", + "turn": 0, + }) + # Send updated state after DM command (e.g. set_hp changes HP) + try: + await ws.send_json({"type": "state", "campaign": _build_state(campaign_id)}) + except WebSocketDisconnect: + break + except Exception as e: + log.exception("DM override failed") + try: + await ws.send_json({"type": "error", "message": str(e)}) + except WebSocketDisconnect: + break + def _build_state(campaign_id: int) -> dict: ctx = GameContext.load(campaign_id) diff --git a/src/web/static/index.html b/src/web/static/index.html index 538a074..9ef6ac9 100644 --- a/src/web/static/index.html +++ b/src/web/static/index.html @@ -291,6 +291,65 @@ html, body { font-style: italic; } +/* DM Override panel */ +.dm-toggle { + cursor: pointer; + user-select: none; + opacity: 0.6; + font-size: 12px; + transition: opacity 0.2s; +} +.dm-toggle:hover { opacity: 1; } +.dm-panel { + padding: 6px 0; + display: flex; + flex-direction: column; + gap: 6px; +} +.dm-panel.hidden { display: none; } +.dm-panel select, +.dm-panel input, +.dm-panel textarea { + width: 100%; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 8px; + color: var(--text-primary); + font-family: var(--font-body); + font-size: 12px; + outline: none; +} +.dm-panel select:focus, +.dm-panel input:focus, +.dm-panel textarea:focus { + border-color: var(--gold-dim); +} +.dm-panel textarea { + min-height: 60px; + resize: vertical; +} +#btn-dm-exec { + background: var(--red); + border: none; + border-radius: var(--radius); + padding: 6px; + color: #fff; + font-family: var(--font-display); + font-size: 11px; + cursor: pointer; + transition: background 0.2s; + text-transform: uppercase; + letter-spacing: 1px; +} +#btn-dm-exec:hover { background: #c04040; } +.dm-result { + font-size: 11px; + color: var(--text-muted); + padding: 4px 0; + min-height: 18px; +} + /* ============================================================ Chat Area ============================================================ */ @@ -591,6 +650,21 @@ html, body {