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 {

Aree

Nessuna area
+ @@ -879,6 +953,100 @@ function renderAreas(areas) { }).join(''); } +// ================================================================ +// DM Override +// ================================================================ +let dmOpen = false; + +function toggleDM() { + dmOpen = !dmOpen; + const panel = $('dm-panel'); + panel.classList.toggle('hidden', !dmOpen); + if (dmOpen) onDMCommand(); +} + +function onDMCommand() { + const cmd = $('dm-cmd').value; + const fields = $('dm-fields'); + $('dm-result').textContent = ''; + switch (cmd) { + case 'narrate': + fields.innerHTML = ''; + break; + case 'set_hp': + fields.innerHTML = ` + + + `; + break; + case 'add_item': + fields.innerHTML = ` + + + `; + break; + case 'force_roll': + fields.innerHTML = ` + + + `; + break; + case 'skip_combat': + fields.innerHTML = ''; + break; + default: + fields.innerHTML = ''; + } +} + +function execDM() { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const cmd = $('dm-cmd').value; + let params = {}; + const resultDiv = $('dm-result'); + resultDiv.textContent = 'Invio...'; + + switch (cmd) { + case 'narrate': { + const text = $('dm-narrate-text')?.value?.trim(); + if (!text) { resultDiv.textContent = 'Inserisci testo.'; return; } + params = { text }; + break; + } + case 'set_hp': { + const cid = parseInt($('dm-char-id')?.value); + const hp = parseInt($('dm-hp-value')?.value); + if (!cid || isNaN(hp)) { resultDiv.textContent = 'Specifica ID PG e PF.'; return; } + params = { character_id: cid, value: hp }; + break; + } + case 'add_item': { + const cid = parseInt($('dm-char-id')?.value); + const item = $('dm-item-name')?.value?.trim(); + if (!cid || !item) { resultDiv.textContent = 'Specifica ID PG e oggetto.'; return; } + params = { character_id: cid, item }; + break; + } + case 'force_roll': { + const die = $('dm-die')?.value?.trim(); + const val = parseInt($('dm-value')?.value); + if (!die || isNaN(val)) { resultDiv.textContent = 'Specifica dado e valore.'; return; } + params = { die, forced_value: val }; + break; + } + case 'skip_combat': { + const aid = $('dm-area-id')?.value?.trim(); + if (!aid) { resultDiv.textContent = 'Specifica ID area.'; return; } + params = { area_id: aid }; + break; + } + } + + ws.send(JSON.stringify({ type: 'dm', command: cmd, params })); + resultDiv.textContent = '✧ Comando inviato.'; +} + + // ================================================================ // Send action // ================================================================