feat(ui): pannello DM-Override nella sidebar
- Handler WS type:dm — esegue dm_override bypassando il LLM - Pannello collassabile con selettore comandi e campi dinamici - Comandi: narrate, set_hp, add_item, force_roll, skip_combat - Stato aggiornato dopo DM override - Implementato narrate con testo personalizzato
This commit is contained in:
1 parent
e9c559c001
commit
886df7429f
3 files changed
+204
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
<h2>Aree</h2>
|
||||
<div id="area-display"><div class="empty-state">Nessuna area</div></div>
|
||||
</div>
|
||||
<div class="sidebar-section" id="dm-section">
|
||||
<h2 class="dm-toggle" onclick="toggleDM()">DM ◥</h2>
|
||||
<div id="dm-panel" class="dm-panel hidden">
|
||||
<select id="dm-cmd" onchange="onDMCommand()">
|
||||
<option value="narrate">Narrate (testo)</option>
|
||||
<option value="set_hp">Set HP</option>
|
||||
<option value="add_item">Add Item</option>
|
||||
<option value="force_roll">Force Roll</option>
|
||||
<option value="skip_combat">Skip Combat</option>
|
||||
</select>
|
||||
<div id="dm-fields"></div>
|
||||
<button id="btn-dm-exec" onclick="execDM()">Esegui</button>
|
||||
<div id="dm-result" class="dm-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN -->
|
||||
@@ -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 = '<textarea id="dm-narrate-text" placeholder="Testo narrativo..."></textarea>';
|
||||
break;
|
||||
case 'set_hp':
|
||||
fields.innerHTML = `
|
||||
<input id="dm-char-id" type="number" placeholder="ID PG" min="1" />
|
||||
<input id="dm-hp-value" type="number" placeholder="PF" min="0" />
|
||||
`;
|
||||
break;
|
||||
case 'add_item':
|
||||
fields.innerHTML = `
|
||||
<input id="dm-char-id" type="number" placeholder="ID PG" min="1" />
|
||||
<input id="dm-item-name" placeholder="Nome oggetto" />
|
||||
`;
|
||||
break;
|
||||
case 'force_roll':
|
||||
fields.innerHTML = `
|
||||
<input id="dm-die" placeholder="dado (es. d20)" />
|
||||
<input id="dm-value" type="number" placeholder="Valore forzato" min="1" />
|
||||
`;
|
||||
break;
|
||||
case 'skip_combat':
|
||||
fields.innerHTML = '<input id="dm-area-id" placeholder="ID area (es. 1.4)" />';
|
||||
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
|
||||
// ================================================================
|
||||
|
||||
Reference in new issue
Block a user