Files

403 lines
19 KiB
HTML

{% extends "base.html" %}
{% block content %}
{% if session %}
<div class="info-bar">
<span><span class="label">Session</span> <strong class="val">#{{ session.id }} — {{ session.name }}</strong></span>
<span><span class="label">Jour</span> <strong class="val">{{ session.current_day }}</strong> <span class="label">/ Tick</span> <strong class="val">{{ session.current_tick }}h</strong></span>
<span class="mode-tag">{{ session.mode | upper }}</span>
{% set st = session.status %}
<span class="status-badge {{ 'status-active' if st=='active' else 'status-stopped' if st=='stopped' else 'status-paused' }}">
<span class="dot {{ 'dot-green' if st=='active' else 'dot-red' if st=='stopped' else 'dot-amber' }}"></span>
{{ st | upper }}
</span>
<span class="label">Seed: <span class="val">{{ session.seed_global }}</span></span>
<span class="refresh-note">
<a href="?sid={{ session.id }}">↻ Actualiser</a>
<span id="countdown"></span>
</span>
</div>
{% endif %}
<div class="metric-row">
<div class="metric-card"><div class="metric-label">PNJ Vivants</div><div class="metric-value metric-green">{{ alive_count }}</div></div>
<div class="metric-card"><div class="metric-label">PNJ Morts</div><div class="metric-value metric-red">{{ stats.get('PNJ morts', 0) }}</div></div>
<div class="metric-card"><div class="metric-label">Total PNJ</div><div class="metric-value metric-muted">{{ total_count }}</div></div>
<div class="metric-card"><div class="metric-label">Combats</div><div class="metric-value metric-amber">{{ stats.get('Combats', 0) }}</div></div>
<div class="metric-card"><div class="metric-label">Rencontres</div><div class="metric-value metric-blue">{{ stats.get('Rencontres', 0) }}</div></div>
<div class="metric-card"><div class="metric-label">Événements</div><div class="metric-value metric-muted">{{ stats.get('Événements total', 0) }}</div></div>
</div>
<div class="grid">
<div class="panel">
<h2>👥 PNJ ({{ alive_count }}/{{ total_count }})</h2>
<div class="scrollable">
<table>
<tr><th>Tier</th><th>Nom</th><th>PV</th><th>Faction</th><th>Zone</th><th>Caps</th></tr>
{% for c in characters %}
{% set pct = (c.hp / c.max_hp * 100) | int if c.max_hp else 0 %}
<tr class="{{ 'dead' if not c.is_alive else '' }}">
<td>
{% if c.tier == 0 %}<span class="tier0-text">★ BOSS</span>
{% elif c.tier == 2 %}<span class="tier2-text">T2</span>
{% elif c.tier == 3 %}<span class="tier3-text">T3</span>
{% else %}<span class="tier1-text">T1</span>{% endif %}
</td>
<td>{{ c.name }}</td>
<td>
{% if c.is_alive %}
<span class="{{ 'hp-crit' if pct < 25 else 'hp-low' if pct < 50 else 'hp-ok' }}">{{ c.hp }}/{{ c.max_hp }}</span>
{% else %}<span class="tier3-text"></span>{% endif %}
</td>
<td>
{% set f = c.faction_slug or 'sans' %}
<span class="faction-{{ 'union' if 'union' in f else 'cda' if 'cda' in f else 'ecumeurs' if 'ecumeur' in f else 'krewe' if 'krewe' in f else 'oak' if 'oak' in f else 'regie' if 'regie' in f else 'syndicat' if 'syndicat' in f else 'default' }}">{{ f[:14] }}</span>
</td>
<td style="font-size:12px;color:var(--muted)">{{ (c.location_slug or '—')[:16] }}</td>
<td style="color:var(--amber)">{{ c.caps }}¢</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="panel">
<h2>📡 Événements récents</h2>
<div class="scrollable">
<table>
<tr><th>J/T</th><th>Type</th><th>Description</th></tr>
{% for ev in events %}
<tr>
<td style="color:var(--muted);white-space:nowrap">{{ ev.day }}/{{ ev.tick }}</td>
<td class="ev-{{ ev.event_type }}" style="white-space:nowrap"><small>{{ ev.event_type }}</small></td>
<td style="font-size:12px">{{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}</td>
</tr>
{% endfor %}
{% if not events %}<tr><td colspan="3" style="color:var(--dim);text-align:center;padding:16px">Aucun événement</td></tr>{% endif %}
</table>
</div>
</div>
</div>
<div class="grid3">
<div class="panel">
<h2>🗺 État du monde</h2>
<div class="scrollable">
{% for ws in world_state %}
<div class="world-zone">
<span class="zone-name">{{ ws.location_slug }}</span>
<span class="zone-stats">
<span>🍖 <span>{{ ws.food_level or 0 }}%</span></span>
<span>💧 <span>{{ ws.water_level or 0 }}%</span></span>
<span>🛡 <span>{{ ws.security_level or 0 }}%</span></span>
<span>👤 <span>{{ ws.pop_count or 0 }}</span></span>
</span>
</div>
{% endfor %}
{% if not world_state %}<p style="color:var(--dim);padding:12px 0">Pas de données monde</p>{% endif %}
</div>
</div>
<div class="panel">
<h2>⚔ Relations factions</h2>
<div class="scrollable">
<table>
<tr><th>Faction A</th><th>Faction B</th><th>Score</th><th>État</th></tr>
{% for r in faction_relations %}
{% set cls = 'rel-hostile' if r.relation_score < -20 else 'rel-allie' if r.relation_score > 20 else 'rel-neutral' %}
<tr>
<td style="font-size:12px">{{ r.faction_a[:14] }}</td>
<td style="font-size:12px">{{ r.faction_b[:14] }}</td>
<td class="{{ cls }}">{{ r.relation_score }}</td>
<td class="{{ cls }}" style="font-size:12px">{{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}</td>
</tr>
{% endfor %}
{% if not faction_relations %}<tr><td colspan="4" style="color:var(--dim)">Pas de données</td></tr>{% endif %}
</table>
</div>
</div>
<div class="panel">
<h2>🎭 PNJ par tier</h2>
<table style="margin-bottom:12px">
<tr><th>Tier</th><th>Label</th><th>Vivants</th><th>Morts</th></tr>
{% for row in tier_stats %}
<tr>
<td class="tier{{ row.tier }}-text">{{ row.tier }}</td>
<td>{{ row.label }}</td>
<td class="metric-green">{{ row.alive }}</td>
<td style="color:var(--muted)">{{ row.dead }}</td>
</tr>
{% endfor %}
</table>
<h2 style="margin-top:4px">🏙 PNJ par zone (top 5)</h2>
<table>
<tr><th>Zone</th><th>PNJ</th></tr>
{% for row in zone_stats %}
<tr>
<td style="font-size:12px">{{ row.location_slug }}</td>
<td class="metric-green">{{ row.cnt }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="panel" style="margin-top:12px">
<h2>⚡ Rencontres récentes</h2>
<div class="scrollable" style="max-height:180px">
<table>
<tr><th>J/T</th><th>Type</th><th>Acteur</th><th>Zone</th><th>Description</th></tr>
{% for ev in encounters %}
<tr>
<td style="color:var(--muted);white-space:nowrap;font-size:12px">{{ ev.day }}/{{ ev.tick }}</td>
<td class="ev-encounter" style="font-size:12px;white-space:nowrap">{{ ev.event_type }}</td>
<td style="font-size:12px">{{ ev.actor_name or '—' }}</td>
<td style="font-size:12px;color:var(--muted)">{{ (ev.location_slug or '—')[:16] }}</td>
<td style="font-size:12px">{{ ev.description[:90] }}{% if ev.description|length > 90 %}…{% endif %}</td>
</tr>
{% endfor %}
{% if not encounters %}<tr><td colspan="5" style="color:var(--dim);text-align:center;padding:12px">Aucune rencontre</td></tr>{% endif %}
</table>
</div>
</div>
{% if boss_inventory %}
<div class="panel" style="margin-top:12px">
<h2>★ Équipement chefs de faction</h2>
<table>
<tr><th>Boss</th><th>Item</th><th>Type</th><th>Qté</th><th>Équipé</th></tr>
{% for row in boss_inventory %}
<tr>
<td class="tier0-text">{{ row.char_name }}</td>
<td>{{ row.item_name }}</td>
<td style="color:var(--muted);font-size:12px">{{ row.item_type }}</td>
<td>{{ row.quantity }}</td>
<td style="color:var(--green)">{{ '✓' if row.is_equipped else '' }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
<!-- Contrôles simulation -->
<div style="margin-top:16px">
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid var(--border);flex-wrap:wrap">
<button class="sim-tab-btn active" id="stab-truth" onclick="showSimTab('truth')">&#128218; Vérité</button>
<button class="sim-tab-btn" id="stab-enriched" onclick="showSimTab('enriched')">&#10024; Enrichie</button>
<button class="sim-tab-btn" id="stab-mix" onclick="showSimTab('mix')">&#9889; MIX</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:8px;padding:4px 6px;flex-wrap:wrap">
<span style="font-size:11px;color:var(--dim)">Tick&nbsp;global&nbsp;:</span>
<div class="radio-group" style="margin:0">
<label><input type="radio" name="sim_tick" value="0"><span>INSTANT</span></label>
<label><input type="radio" name="sim_tick" value="5" checked><span>5s</span></label>
<label><input type="radio" name="sim_tick" value="30"><span>30s</span></label>
<label><input type="radio" name="sim_tick" value="60"><span>60s</span></label>
<label><input type="radio" name="sim_tick" value="3600"><span>1h</span></label>
</div>
</div>
</div>
<div id="simtab-truth" class="sim-tab-content" style="display:block">
<div class="panel" style="margin-top:12px;padding:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
<span class="dot dot-red" id="dot-sim-truth"></span>
<span style="font-weight:600">&#128218; Sim Vérité</span>
<span style="font-size:10px;color:var(--dim)">sources : fallout_lore + fallout_sim_rules uniquement</span>
</div>
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:12px">
<span style="font-size:12px;color:var(--dim)">Session : <strong style="color:var(--text)">#{{ current_sid }}</strong></span>
<label style="font-size:12px;color:var(--dim)">Mode :
<select id="truth-mode" onchange="setSessionMode(this.value)" style="background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:12px;padding:2px 8px;margin-left:4px">
{% for m in ['pacifiste','politique','guerre_commerciale','guerre','survie_extreme'] %}
<option value="{{ m }}" {{ 'selected' if session and session.mode==m else '' }}>{{ m }}</option>
{% endfor %}
</select>
</label>
</div>
<div id="sim-truth-status" style="font-size:12px;color:var(--dim);margin-bottom:10px">Arrêtée</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-green" id="btn-truth-start" onclick="simStart('truth')">&#9654; Démarrer</button>
<button class="btn btn-red" id="btn-truth-stop" onclick="simStop('truth')">&#9632; Arrêter</button>
<button class="btn" onclick="simDownloadLog('truth')" style="font-size:11px">&#11015; Log JSON</button>
</div>
<div id="console-sim-truth" class="mini-console" style="display:none;margin-top:12px;max-height:320px"></div>
</div>
</div>
<div id="simtab-enriched" class="sim-tab-content" style="display:none">
<div class="panel" style="margin-top:12px;padding:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
<span class="dot dot-red" id="dot-sim-enriched"></span>
<span style="font-weight:600;color:var(--amber)">&#10024; Sim Enrichie</span>
<span style="font-size:10px;color:var(--dim)">+ fallout_lore_enriched + fallout_sim_enriched</span>
</div>
<div id="sim-enriched-status" style="font-size:12px;color:var(--dim);margin-bottom:10px">Arrêtée</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-green" id="btn-enriched-start" onclick="simStart('enriched')">&#9654; Démarrer</button>
<button class="btn btn-red" id="btn-enriched-stop" onclick="simStop('enriched')">&#9632; Arrêter</button>
<button class="btn" onclick="simDownloadLog('enriched')" style="font-size:11px">&#11015; Log JSON</button>
</div>
<div id="console-sim-enriched" class="mini-console" style="display:none;margin-top:12px;max-height:320px"></div>
</div>
</div>
<div id="simtab-mix" class="sim-tab-content" style="display:none">
<div class="panel" style="margin-top:12px;padding:16px">
<div style="margin-bottom:12px">
<span style="font-weight:600;color:var(--green)">&#9889; Mode MIX — Comparaison simultanée</span>
<p style="font-size:12px;color:var(--dim);margin:6px 0 0">Lance les deux simulations en même temps.</p>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px">
<button class="btn btn-green" id="btn-mix-start" onclick="simMixStart()">&#9654; Lancer les 2</button>
<button class="btn btn-red" id="btn-mix-stop" onclick="simMixStop()">&#9632; Arrêter les 2</button>
<button class="btn" onclick="simDownloadLog('truth')" style="font-size:11px">&#11015; Log Vérité</button>
<button class="btn" onclick="simDownloadLog('enriched')" style="font-size:11px">&#11015; Log Enrichie</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<div style="font-size:11px;color:var(--dim);margin-bottom:6px;display:flex;align-items:center;gap:6px">
<span class="dot dot-red" id="dot-mix-truth"></span>&#128218; Vérité
</div>
<div id="console-mix-truth" class="mini-console" style="max-height:260px"></div>
</div>
<div>
<div style="font-size:11px;color:var(--amber);margin-bottom:6px;display:flex;align-items:center;gap:6px">
<span class="dot dot-red" id="dot-mix-enriched"></span>&#10024; Enrichie
</div>
<div id="console-mix-enriched" class="mini-console" style="max-height:260px"></div>
</div>
</div>
</div>
</div>
</div>
<script>
var _simPolls = {truth: null, enriched: null};
var _simLastLines = {truth: 0, enriched: 0};
var _mixLastLines = {truth: 0, enriched: 0};
function showSimTab(tab) {
['truth','enriched','mix'].forEach(function(t) {
document.getElementById('simtab-' + t).style.display = (t === tab) ? 'block' : 'none';
document.getElementById('stab-' + t).classList.toggle('active', t === tab);
});
}
function simStart(mode) {
var tickEl = document.querySelector('input[name="sim_tick"]:checked');
var tickSpeed = tickEl ? parseInt(tickEl.value) : 5;
fetch('/api/sim/' + mode + '/run', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({sid: {{ current_sid }}, tick_speed_s: tickSpeed})
}).then(function(r){ return r.json(); }).then(function(d){
if (d.error) { alert(d.error); return; }
_simLastLines[mode] = 0;
var cons = document.getElementById('console-sim-' + mode);
cons.textContent = '';
cons.style.display = 'block';
document.getElementById('dot-sim-' + mode).className = 'dot dot-green';
document.getElementById('sim-' + mode + '-status').textContent = 'En cours — ' + (tickSpeed === 0 ? 'INSTANT' : tickSpeed + 's') + '/tick';
document.getElementById('btn-' + mode + '-start').disabled = true;
if (_simPolls[mode]) clearInterval(_simPolls[mode]);
_simPolls[mode] = setInterval(function(){ simPoll(mode); }, 2000);
});
}
function simPoll(mode) {
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
var cons = document.getElementById('console-sim-' + mode);
var lines = d.output || [];
if (lines.length > _simLastLines[mode]) {
cons.textContent += lines.slice(_simLastLines[mode]).join('\n') + '\n';
cons.scrollTop = cons.scrollHeight;
_simLastLines[mode] = lines.length;
}
if (!d.running) {
clearInterval(_simPolls[mode]);
document.getElementById('dot-sim-' + mode).className = 'dot ' + (d.returncode === 0 ? 'dot-amber' : 'dot-red');
document.getElementById('sim-' + mode + '-status').textContent = d.error || ('Arrêtée (code ' + d.returncode + ')');
document.getElementById('btn-' + mode + '-start').disabled = false;
}
});
}
function simStop(mode) {
fetch('/api/sim/' + mode + '/stop', {method:'POST'}).then(function(){ simPoll(mode); });
}
function simDownloadLog(mode) { window.location.href = '/api/sim/' + mode + '/log'; }
function simMixStart() {
var tickEl = document.querySelector('input[name="sim_tick"]:checked');
var tickSpeed = tickEl ? parseInt(tickEl.value) : 5;
['truth','enriched'].forEach(function(mode) {
fetch('/api/sim/' + mode + '/run', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({sid: {{ current_sid }}, tick_speed_s: tickSpeed})
}).then(function(r){ return r.json(); }).then(function(d){
if (d.error) { console.warn('[MIX] ' + mode + ':', d.error); return; }
_mixLastLines[mode] = 0;
document.getElementById('console-mix-' + mode).textContent = '';
document.getElementById('dot-mix-' + mode).className = 'dot dot-green';
document.getElementById('btn-mix-start').disabled = true;
if (_simPolls[mode]) clearInterval(_simPolls[mode]);
_simPolls[mode] = setInterval(function(){ simMixPoll(mode); }, 2000);
});
});
}
function simMixPoll(mode) {
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
var cons = document.getElementById('console-mix-' + mode);
var lines = d.output || [];
if (lines.length > _mixLastLines[mode]) {
cons.textContent += lines.slice(_mixLastLines[mode]).join('\n') + '\n';
cons.scrollTop = cons.scrollHeight;
_mixLastLines[mode] = lines.length;
}
if (!d.running) {
clearInterval(_simPolls[mode]);
document.getElementById('dot-mix-' + mode).className = 'dot ' + (d.returncode === 0 ? 'dot-amber' : 'dot-red');
}
});
}
function simMixStop() { ['truth','enriched'].forEach(function(mode){ simStop(mode); }); }
function setSessionMode(mode) {
fetch('/api/session/set-mode', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({sid: {{ current_sid }}, mode: mode})
}).then(function(r){ return r.json(); }).then(function(d){
if (d.error) alert('Erreur : ' + d.error);
});
}
(function(){
['truth','enriched'].forEach(function(mode){
fetch('/api/sim/' + mode + '/status').then(function(r){ return r.json(); }).then(function(d){
if (d.running) {
document.getElementById('dot-sim-' + mode).className = 'dot dot-green';
document.getElementById('sim-' + mode + '-status').textContent = 'En cours';
document.getElementById('btn-' + mode + '-start').disabled = true;
document.getElementById('console-sim-' + mode).style.display = 'block';
_simLastLines[mode] = (d.output||[]).length;
_simPolls[mode] = setInterval(function(){ simPoll(mode); }, 2000);
}
});
});
})();
var t = 30;
var el = document.getElementById('countdown');
if (el) {
setInterval(function() {
t--;
el.textContent = ' (' + t + 's)';
if (t <= 0) { location.reload(); }
}, 1000);
}
</script>
{% endblock %}