feat: dashboard VAULTCOM v4 — onglets session + paramètres
Onglet SIMULATION:
- Switcher de session en haut (S1 / S2 / ...) avec indicateur status vert/rouge
- Countdown JS 30s avant auto-refresh (au lieu de meta http-equiv)
- Filtre cycle_tick des événements (trop verbeux)
Onglet PARAMÈTRES (/params?sid=N):
- Formulaire config: nom session, mode, tick_sleep, drain_speed, fatigue_hp,
encounter_rate, caps_multiplier, survie/rencontres activées
- POST /params/save → écrit sim_001.json / sim_002.json + UPDATE sessions SET mode
- POST /params/setstatus → active/pause/stop via DB (run.py sort au prochain tick)
- POST /params/resetday → remet current_day/current_tick en DB
- Tableau des modes avec drain/encounter/caps pour comparaison rapide
- Snippet de commande SSH pour relancer chaque session depuis terminal
Docker: monté /home/ubuntu/fallout-venice/src/config en rw dans le container
→ /params/save peut écrire les JSON directement
This commit is contained in:
+437
-95
@@ -1,12 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
VAULTCOM — Fallout: Venice of Wasteland
|
VAULTCOM — Fallout: Venice of Wasteland
|
||||||
Dashboard simulation. Schéma DB actuel (migration_003).
|
Dashboard simulation. Schéma DB actuel (migration_003).
|
||||||
|
Onglets: Simulation (par session) | Paramètres (config + restart)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, json
|
import os, json, subprocess
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
from flask import Flask, render_template_string, jsonify
|
from flask import Flask, render_template_string, jsonify, request, redirect, url_for
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ DB_NAME = os.getenv("DB_NAME", "fallout")
|
|||||||
DB_USER = os.getenv("DB_USER", "fallout")
|
DB_USER = os.getenv("DB_USER", "fallout")
|
||||||
DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!")
|
DB_PASS = os.getenv("DB_PASS", "VeniceOfWasteland2026!")
|
||||||
|
|
||||||
|
SIM_CONFIGS_DIR = os.getenv("SIM_CONFIGS_DIR", "/app/src/config")
|
||||||
|
|
||||||
def get_conn():
|
def get_conn():
|
||||||
return psycopg2.connect(
|
return psycopg2.connect(
|
||||||
host=DB_HOST, port=DB_PORT, dbname=DB_NAME,
|
host=DB_HOST, port=DB_PORT, dbname=DB_NAME,
|
||||||
@@ -35,8 +38,33 @@ def query_one(sql, params=None):
|
|||||||
rows = query(sql, params)
|
rows = query(sql, params)
|
||||||
return rows[0] if rows else None
|
return rows[0] if rows else None
|
||||||
|
|
||||||
|
def execute(sql, params=None):
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, params)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def load_sim_config(session_id: int) -> dict:
|
||||||
|
path = os.path.join(SIM_CONFIGS_DIR, f"sim_{session_id:03d}.json")
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_sim_config(session_id: int, cfg: dict):
|
||||||
|
path = os.path.join(SIM_CONFIGS_DIR, f"sim_{session_id:03d}.json")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def get_all_sessions():
|
||||||
|
return query("SELECT * FROM sessions ORDER BY id")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# HTML template
|
# HTML Template
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
TEMPLATE = r"""<!DOCTYPE html>
|
TEMPLATE = r"""<!DOCTYPE html>
|
||||||
@@ -44,32 +72,33 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="refresh" content="30">
|
|
||||||
<title>VAULTCOM — Fallout: Venice of Wasteland</title>
|
<title>VAULTCOM — Fallout: Venice of Wasteland</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--pip-green: #00ff41;
|
--pip-green: #00ff41; --pip-amber: #ffb347; --pip-red: #ff4444;
|
||||||
--pip-amber: #ffb347;
|
--pip-blue: #44aaff; --pip-dim: #003300; --bg: #0a0a0a;
|
||||||
--pip-red: #ff4444;
|
--panel: #0d1a0d; --border: #1a3d1a;
|
||||||
--pip-blue: #44aaff;
|
|
||||||
--pip-dim: #003300;
|
|
||||||
--bg: #0a0a0a;
|
|
||||||
--panel: #0d1a0d;
|
|
||||||
--border: #1a3d1a;
|
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
body { background: var(--bg); color: var(--pip-green);
|
||||||
background: var(--bg);
|
font-family: 'Courier New', monospace; font-size: 13px; padding: 8px; }
|
||||||
color: var(--pip-green);
|
h1 { font-size: 18px; letter-spacing: 4px; text-align: center; padding: 8px 0;
|
||||||
font-family: 'Courier New', monospace;
|
border-bottom: 1px solid var(--pip-green); margin-bottom: 8px; }
|
||||||
font-size: 13px;
|
|
||||||
padding: 8px;
|
/* ---- NAV TABS ---- */
|
||||||
}
|
.nav { display: flex; gap: 4px; margin-bottom: 10px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
||||||
h1 { font-size: 20px; letter-spacing: 4px; text-align: center; padding: 10px 0;
|
.nav a { padding: 4px 14px; text-decoration: none; color: #555;
|
||||||
border-bottom: 1px solid var(--pip-green); margin-bottom: 10px; }
|
border: 1px solid #222; font-size: 12px; letter-spacing: 1px; }
|
||||||
|
.nav a.active { color: var(--pip-amber); border-color: var(--pip-amber); background: #1a1000; }
|
||||||
|
.nav a:hover:not(.active) { color: var(--pip-green); border-color: var(--border); }
|
||||||
|
.nav-sep { flex-grow: 1; }
|
||||||
|
.nav .session-btn { border-color: var(--border); }
|
||||||
|
.nav .session-btn.active { color: var(--pip-green); border-color: var(--pip-green); background: #001a00; }
|
||||||
|
|
||||||
|
/* ---- LAYOUT ---- */
|
||||||
.header-bar { display: flex; justify-content: space-between; align-items: center;
|
.header-bar { display: flex; justify-content: space-between; align-items: center;
|
||||||
padding: 4px 8px; background: var(--pip-dim); border: 1px solid var(--border);
|
padding: 4px 8px; background: var(--pip-dim); border: 1px solid var(--border);
|
||||||
margin-bottom: 10px; font-size: 11px; color: var(--pip-amber); }
|
margin-bottom: 8px; font-size: 11px; color: var(--pip-amber); flex-wrap: wrap; gap: 4px; }
|
||||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-top: 10px; }
|
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-top: 10px; }
|
||||||
.panel { background: var(--panel); border: 1px solid var(--border); padding: 8px; }
|
.panel { background: var(--panel); border: 1px solid var(--border); padding: 8px; }
|
||||||
@@ -80,10 +109,8 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
border-bottom: 1px solid var(--border); }
|
border-bottom: 1px solid var(--border); }
|
||||||
td { padding: 2px 4px; font-size: 11px; border-bottom: 1px dashed #0f2a0f; vertical-align: top; }
|
td { padding: 2px 4px; font-size: 11px; border-bottom: 1px dashed #0f2a0f; vertical-align: top; }
|
||||||
tr:hover td { background: #0f200f; }
|
tr:hover td { background: #0f200f; }
|
||||||
.tier0 { color: #ff6600; }
|
.tier0 { color: #ff6600; } .tier1 { color: var(--pip-green); }
|
||||||
.tier1 { color: var(--pip-green); }
|
.tier2 { color: var(--pip-blue); } .tier3 { color: #aaaaaa; }
|
||||||
.tier2 { color: var(--pip-blue); }
|
|
||||||
.tier3 { color: #aaaaaa; }
|
|
||||||
.dead { color: #444; text-decoration: line-through; }
|
.dead { color: #444; text-decoration: line-through; }
|
||||||
.hp-bar { display: inline-block; width: 60px; height: 8px; background: #111; border: 1px solid #333; vertical-align: middle; }
|
.hp-bar { display: inline-block; width: 60px; height: 8px; background: #111; border: 1px solid #333; vertical-align: middle; }
|
||||||
.hp-fill { height: 100%; background: var(--pip-green); }
|
.hp-fill { height: 100%; background: var(--pip-green); }
|
||||||
@@ -99,7 +126,6 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
.event-encounter { color: var(--pip-amber); }
|
.event-encounter { color: var(--pip-amber); }
|
||||||
.event-survival { color: #88cc88; }
|
.event-survival { color: #88cc88; }
|
||||||
.event-death { color: #ff0000; font-weight: bold; }
|
.event-death { color: #ff0000; font-weight: bold; }
|
||||||
.event-loot { color: #88aaff; }
|
|
||||||
.relation-hostile { color: var(--pip-red); }
|
.relation-hostile { color: var(--pip-red); }
|
||||||
.relation-neutral { color: #888; }
|
.relation-neutral { color: #888; }
|
||||||
.relation-allie { color: var(--pip-green); }
|
.relation-allie { color: var(--pip-green); }
|
||||||
@@ -114,24 +140,75 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
.stat-item { font-size: 10px; color: #888; }
|
.stat-item { font-size: 10px; color: #888; }
|
||||||
.stat-item span { color: var(--pip-green); }
|
.stat-item span { color: var(--pip-green); }
|
||||||
.footer { text-align: center; font-size: 10px; color: #333; padding: 10px 0; }
|
.footer { text-align: center; font-size: 10px; color: #333; padding: 10px 0; }
|
||||||
|
.refresh-note { font-size: 10px; color: #555; }
|
||||||
|
|
||||||
|
/* ---- PARAMS TAB ---- */
|
||||||
|
.cfg-block { margin-bottom: 12px; }
|
||||||
|
.cfg-block h3 { font-size: 11px; color: var(--pip-amber); letter-spacing: 1px;
|
||||||
|
border-bottom: 1px solid var(--border); padding-bottom: 3px; margin-bottom: 6px; }
|
||||||
|
.cfg-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||||
|
.cfg-row label { min-width: 220px; font-size: 11px; color: #aaa; }
|
||||||
|
.cfg-row input[type=number], .cfg-row input[type=text], .cfg-row select {
|
||||||
|
background: #001a00; color: var(--pip-green); border: 1px solid var(--border);
|
||||||
|
padding: 2px 6px; font-family: 'Courier New', monospace; font-size: 11px; width: 120px; }
|
||||||
|
.cfg-row input[type=checkbox] { accent-color: var(--pip-green); }
|
||||||
|
.btn { padding: 5px 16px; border: 1px solid var(--pip-amber); background: #1a1000;
|
||||||
|
color: var(--pip-amber); font-family: 'Courier New', monospace; font-size: 12px;
|
||||||
|
cursor: pointer; letter-spacing: 1px; margin-top: 4px; }
|
||||||
|
.btn:hover { background: #2a2000; }
|
||||||
|
.btn-red { border-color: var(--pip-red); color: var(--pip-red); background: #1a0000; }
|
||||||
|
.btn-red:hover { background: #2a0000; }
|
||||||
|
.btn-green { border-color: var(--pip-green); color: var(--pip-green); background: #001a00; }
|
||||||
|
.btn-green:hover { background: #002a00; }
|
||||||
|
.msg-ok { color: var(--pip-green); font-size: 11px; }
|
||||||
|
.msg-err { color: var(--pip-red); font-size: 11px; }
|
||||||
|
.mode-badge { display: inline-block; padding: 2px 8px; border: 1px solid var(--pip-amber);
|
||||||
|
color: var(--pip-amber); font-size: 11px; letter-spacing: 1px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>⚡ VAULTCOM — VENICE OF WASTELAND ⚡</h1>
|
<h1>⚡ VAULTCOM — VENICE OF WASTELAND ⚡</h1>
|
||||||
|
|
||||||
|
<!-- ===== NAVIGATION ===== -->
|
||||||
|
<div class="nav">
|
||||||
|
{% for s in all_sessions %}
|
||||||
|
<a href="/?sid={{ s.id }}" class="session-btn {{ 'active' if s.id == current_sid else '' }}">
|
||||||
|
S{{ s.id }} — {{ s.name[:22] }}
|
||||||
|
<span style="font-size:9px;color:{{ '#00ff41' if s.status=='active' else '#ff4444' }}">
|
||||||
|
● {{ s.status[:3].upper() }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
<span class="nav-sep"></span>
|
||||||
|
<a href="/?sid={{ current_sid }}" class="{{ 'active' if tab=='sim' else '' }}">📡 SIMULATION</a>
|
||||||
|
<a href="/params?sid={{ current_sid }}" class="{{ 'active' if tab=='params' else '' }}">⚙ PARAMÈTRES</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if flash_msg %}
|
||||||
|
<div style="padding:6px 10px; margin-bottom:8px; border:1px solid {{ '#00ff41' if flash_ok else '#ff4444' }}; color:{{ '#00ff41' if flash_ok else '#ff4444' }}; font-size:11px;">
|
||||||
|
{{ flash_msg }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tab == 'sim' %}
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
<!-- ONGLET SIMULATION -->
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
|
||||||
{% if session %}
|
{% if session %}
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span>SESSION #{{ session.id }} — {{ session.name }}</span>
|
<span>SESSION #{{ session.id }} — {{ session.name }}</span>
|
||||||
<span>JOUR {{ session.current_day }} / TICK {{ session.current_tick }}</span>
|
<span>JOUR {{ session.current_day }} / TICK {{ session.current_tick }}h</span>
|
||||||
<span>MODE: {{ session.mode | upper }} | STATUS: {{ session.status | upper }}</span>
|
<span class="mode-badge">{{ session.mode | upper }}</span>
|
||||||
|
<span>STATUS: <strong>{{ session.status | upper }}</strong></span>
|
||||||
<span>SEED: {{ session.seed_global }}</span>
|
<span>SEED: {{ session.seed_global }}</span>
|
||||||
<span>🔄 Actualisé toutes les 30s</span>
|
<span class="refresh-note">🔄 <a href="?sid={{ session.id }}" style="color:#555;text-decoration:none">Actualiser</a>
|
||||||
|
<span id="countdown"></span></span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
<!-- PNJ -->
|
||||||
<!-- PNJ par tier -->
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>👥 PNJ ACTIFS ({{ alive_count }}/{{ total_count }})</h2>
|
<h2>👥 PNJ ACTIFS ({{ alive_count }}/{{ total_count }})</h2>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
@@ -156,8 +233,7 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
<td>
|
<td>
|
||||||
{% set f = c.faction_slug or 'sans' %}
|
{% set f = c.faction_slug or 'sans' %}
|
||||||
<span class="badge badge-{{ 'union' if 'union' in f else 'cda' if 'cda' in f else 'ecumeurs' if 'ecumeur' in f else 'default' }}">
|
<span class="badge badge-{{ 'union' if 'union' in f else 'cda' if 'cda' in f else 'ecumeurs' if 'ecumeur' in f else 'default' }}">
|
||||||
{{ f[:12] }}
|
{{ f[:12] }}</span>
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td><small>{{ (c.location_slug or '—')[:14] }}</small></td>
|
<td><small>{{ (c.location_slug or '—')[:14] }}</small></td>
|
||||||
<td>{{ c.caps }}¢</td>
|
<td>{{ c.caps }}¢</td>
|
||||||
@@ -167,9 +243,9 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Événements récents -->
|
<!-- Événements -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>📡 ÉVÉNEMENTS RÉCENTS (J{{ session.current_day if session else '?' }})</h2>
|
<h2>📡 ÉVÉNEMENTS RÉCENTS</h2>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>J/T</th><th>TYPE</th><th>DESCRIPTION</th></tr>
|
<tr><th>J/T</th><th>TYPE</th><th>DESCRIPTION</th></tr>
|
||||||
@@ -184,12 +260,10 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div><!-- /grid -->
|
|
||||||
|
|
||||||
<div class="grid3">
|
<div class="grid3">
|
||||||
|
<!-- World state -->
|
||||||
<!-- World state par zone -->
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>🗺 ÉTAT DU MONDE</h2>
|
<h2>🗺 ÉTAT DU MONDE</h2>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
@@ -208,7 +282,7 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Relations faction -->
|
<!-- Relations factions -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>⚔ RELATIONS FACTIONS</h2>
|
<h2>⚔ RELATIONS FACTIONS</h2>
|
||||||
<div class="scrollable">
|
<div class="scrollable">
|
||||||
@@ -219,8 +293,7 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
<td><small>{{ r.faction_a[:12] }}</small></td>
|
<td><small>{{ r.faction_a[:12] }}</small></td>
|
||||||
<td><small>{{ r.faction_b[:12] }}</small></td>
|
<td><small>{{ r.faction_b[:12] }}</small></td>
|
||||||
<td class="{{ 'relation-hostile' if r.relation_score < -20 else 'relation-allie' if r.relation_score > 20 else 'relation-neutral' }}">
|
<td class="{{ 'relation-hostile' if r.relation_score < -20 else 'relation-allie' if r.relation_score > 20 else 'relation-neutral' }}">
|
||||||
{{ r.relation_score }}
|
{{ r.relation_score }}</td>
|
||||||
</td>
|
|
||||||
<td><small class="{{ 'relation-hostile' if r.relation_score < -20 else 'relation-allie' if r.relation_score > 20 else 'relation-neutral' }}">
|
<td><small class="{{ 'relation-hostile' if r.relation_score < -20 else 'relation-allie' if r.relation_score > 20 else 'relation-neutral' }}">
|
||||||
{{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}
|
{{ r.relation_label or ('HOSTILE' if r.relation_score < -20 else 'ALLIÉ' if r.relation_score > 20 else 'NEUTRE') }}
|
||||||
</small></td>
|
</small></td>
|
||||||
@@ -231,7 +304,7 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats simulation -->
|
<!-- Stats -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>📊 STATS SIMULATION</h2>
|
<h2>📊 STATS SIMULATION</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -240,21 +313,17 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
<tr><td>{{ k }}</td><td class="tier1">{{ v }}</td></tr>
|
<tr><td>{{ k }}</td><td class="tier1">{{ v }}</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<h2 style="margin-top:6px">🎭 PNJ PAR TIER</h2>
|
<h2 style="margin-top:6px">🎭 PNJ PAR TIER</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>TIER</th><th>LABEL</th><th>VIVANTS</th><th>MORTS</th></tr>
|
<tr><th>TIER</th><th>LABEL</th><th>VIVANTS</th><th>MORTS</th></tr>
|
||||||
{% for row in tier_stats %}
|
{% for row in tier_stats %}
|
||||||
<tr class="tier{{ row.tier }}">
|
<tr class="tier{{ row.tier }}">
|
||||||
<td>{{ row.tier }}</td>
|
<td>{{ row.tier }}</td><td>{{ row.label }}</td>
|
||||||
<td>{{ row.label }}</td>
|
<td>{{ row.alive }}</td><td class="tier3">{{ row.dead }}</td>
|
||||||
<td>{{ row.alive }}</td>
|
|
||||||
<td class="tier3">{{ row.dead }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<h2 style="margin-top:6px">🏙 PNJ PAR ZONE (TOP 5)</h2>
|
<h2 style="margin-top:6px">🏙 PNJ PAR ZONE (TOP 5)</h2>
|
||||||
<table>
|
<table>
|
||||||
@@ -264,10 +333,9 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /grid3 -->
|
<!-- Encounters -->
|
||||||
|
|
||||||
<!-- Encounters récents -->
|
|
||||||
<div class="panel" style="margin-top:10px">
|
<div class="panel" style="margin-top:10px">
|
||||||
<h2>⚡ RENCONTRES RÉCENTES</h2>
|
<h2>⚡ RENCONTRES RÉCENTES</h2>
|
||||||
<div class="scrollable" style="max-height:160px">
|
<div class="scrollable" style="max-height:160px">
|
||||||
@@ -287,7 +355,7 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Inventaire boss (tier 0) -->
|
<!-- Boss inventory -->
|
||||||
{% if boss_inventory %}
|
{% if boss_inventory %}
|
||||||
<div class="panel" style="margin-top:10px">
|
<div class="panel" style="margin-top:10px">
|
||||||
<h2>★ ÉQUIPEMENT CHEFS DE FACTION</h2>
|
<h2>★ ÉQUIPEMENT CHEFS DE FACTION</h2>
|
||||||
@@ -306,18 +374,189 @@ TEMPLATE = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="footer">VAULT-TEC VAULTCOM v3.0 | Fallout: Venice of Wasteland | J={{ session.current_day if session else '?' }} T={{ session.current_tick if session else '?' }}</div>
|
<script>
|
||||||
|
// Countdown auto-refresh 30s
|
||||||
|
var t = 30;
|
||||||
|
var el = document.getElementById('countdown');
|
||||||
|
if (el) {
|
||||||
|
setInterval(function() {
|
||||||
|
t--;
|
||||||
|
el.textContent = ' (' + t + 's)';
|
||||||
|
if (t <= 0) { location.reload(); }
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% elif tab == 'params' %}
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
<!-- ONGLET PARAMÈTRES -->
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
|
||||||
|
{% if session %}
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>SESSION #{{ session.id }} — {{ session.name }}</span>
|
||||||
|
<span class="mode-badge">{{ session.mode | upper }}</span>
|
||||||
|
<span>STATUS: <strong style="color:{{ '#00ff41' if session.status=='active' else '#ff4444' }}">{{ session.status | upper }}</strong></span>
|
||||||
|
<span>JOUR {{ session.current_day }} / TICK {{ session.current_tick }}h</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<!-- Config JSON éditable -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>⚙ CONFIG SIM_{{ '%03d' % current_sid }}.JSON</h2>
|
||||||
|
<form method="POST" action="/params/save?sid={{ current_sid }}">
|
||||||
|
{% if cfg %}
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>GÉNÉRAL</h3>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Nom de la session</label>
|
||||||
|
<input type="text" name="name" value="{{ cfg.get('name','') }}" style="width:220px">
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Mode actif</label>
|
||||||
|
<select name="mode">
|
||||||
|
{% for m in ['pacifiste','politique','guerre_commerciale','guerre','survie_extreme'] %}
|
||||||
|
<option value="{{ m }}" {{ 'selected' if cfg.get('mode')==m else '' }}>{{ m }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Tick sleep (secondes)</label>
|
||||||
|
<input type="number" name="tick_sleep_sec" value="{{ cfg.get('tick',{}).get('tick_sleep_sec',60) }}" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>SURVIE</h3>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>drain_speed (multiplicateur)</label>
|
||||||
|
<input type="number" name="survival_drain_speed" value="{{ cfg.get('survival',{}).get('drain_speed',1.0) }}" step="0.1" min="0.1" max="5">
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>fatigue_hp_per_2pts</label>
|
||||||
|
<input type="number" name="survival_fatigue_hp" value="{{ cfg.get('survival',{}).get('fatigue_hp_per_2pts',2) }}" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Survie activée</label>
|
||||||
|
<input type="checkbox" name="survival_enabled" {{ 'checked' if cfg.get('survival',{}).get('enabled',True) else '' }}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>RENCONTRES</h3>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>global_rate_multiplier</label>
|
||||||
|
<input type="number" name="encounter_rate" value="{{ cfg.get('encounter',{}).get('global_rate_multiplier',1.0) }}" step="0.1" min="0" max="5">
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Rencontres activées</label>
|
||||||
|
<input type="checkbox" name="encounter_enabled" {{ 'checked' if cfg.get('encounter',{}).get('enabled',True) else '' }}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>ÉCONOMIE</h3>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>caps_global_multiplier</label>
|
||||||
|
<input type="number" name="caps_multiplier" value="{{ cfg.get('economy',{}).get('caps_global_multiplier',1.0) }}" step="0.1" min="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-green">💾 SAUVEGARDER CONFIG</button>
|
||||||
|
{% else %}
|
||||||
|
<p style="color:#ff4444;font-size:11px">Fichier sim_{{ '%03d' % current_sid }}.json introuvable dans {{ sim_configs_dir }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions session -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🔧 GESTION SESSION #{{ current_sid }}</h2>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>STATUT DB</h3>
|
||||||
|
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||||
|
<input type="hidden" name="status" value="active">
|
||||||
|
<button type="submit" class="btn btn-green">▶ ACTIVER</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||||
|
<input type="hidden" name="status" value="paused">
|
||||||
|
<button type="submit" class="btn">⏸ PAUSE</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="/params/setstatus?sid={{ current_sid }}" style="display:inline">
|
||||||
|
<input type="hidden" name="status" value="stopped">
|
||||||
|
<button type="submit" class="btn btn-red">■ STOP</button>
|
||||||
|
</form>
|
||||||
|
<p style="margin-top:6px;font-size:10px;color:#555">
|
||||||
|
Note: ne tue pas le process Python. Mettre "paused" ou "stopped" fait sortir run.py au prochain tick.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>RESET JOUR/TICK</h3>
|
||||||
|
<form method="POST" action="/params/resetday?sid={{ current_sid }}">
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Remettre au Jour</label>
|
||||||
|
<input type="number" name="day" value="1" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="cfg-row">
|
||||||
|
<label>Tick</label>
|
||||||
|
<input type="number" name="tick" value="0" min="0" max="23">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">⏮ RESET POSITION</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>MODES DISPONIBLES</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>MODE</th><th>DRAIN</th><th>ENCOUNTERS</th><th>CAPS</th></tr>
|
||||||
|
{% for m_name, m_vals in modes.items() %}
|
||||||
|
<tr>
|
||||||
|
<td class="{{ 'tier1' if m_name == cfg.get('mode','') else '' }}">
|
||||||
|
{{ m_name }}{% if m_name == cfg.get('mode','') %} ◀{% endif %}</td>
|
||||||
|
<td class="tier3"><small>×{{ m_vals.get('survival',{}).get('drain_speed','—') }}</small></td>
|
||||||
|
<td class="tier3"><small>×{{ m_vals.get('encounter',{}).get('global_rate_multiplier','—') }}</small></td>
|
||||||
|
<td class="tier3"><small>×{{ m_vals.get('economy',{}).get('caps_global_multiplier','—') }}</small></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cfg-block">
|
||||||
|
<h3>COMMANDES AMPÈRE</h3>
|
||||||
|
<p style="font-size:10px;color:#666;margin-bottom:6px">Copier-coller dans votre terminal SSH :</p>
|
||||||
|
<div style="background:#001000;border:1px solid var(--border);padding:6px;font-size:10px;color:#88aa88;">
|
||||||
|
<p style="color:#555"># Lancer/relancer S{{ current_sid }} :</p>
|
||||||
|
<p>cd /home/ubuntu/fallout-venice/src/engine</p>
|
||||||
|
<p>nohup env SESSION_ID={{ current_sid }} SIM_CONFIG=../config/sim_{{ '%03d' % current_sid }}.json TICK_SLEEP_SEC=60 PYTHONUNBUFFERED=1 python3 -u run.py > ~/fallout_sim_s{{ current_sid }}.log 2>&1 &</p>
|
||||||
|
<br>
|
||||||
|
<p style="color:#555"># Voir les logs :</p>
|
||||||
|
<p>tail -f ~/fallout_sim_s{{ current_sid }}.log</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /grid -->
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="footer">VAULT-TEC VAULTCOM v4.0 | S{{ current_sid }} | J={{ session.current_day if session else '?' }} T={{ session.current_tick if session else '?' }}h</div>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Routes
|
# Helpers data
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.route("/")
|
def get_sim_data(sid: int) -> dict:
|
||||||
def index():
|
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
|
||||||
|
if not session:
|
||||||
session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
|
session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
|
||||||
sid = session["id"] if session else 1
|
|
||||||
|
|
||||||
characters = query("""
|
characters = query("""
|
||||||
SELECT c.*, COALESCE(c.tier, 1) as tier
|
SELECT c.*, COALESCE(c.tier, 1) as tier
|
||||||
@@ -325,40 +564,31 @@ def index():
|
|||||||
WHERE c.session_id = %s
|
WHERE c.session_id = %s
|
||||||
ORDER BY COALESCE(c.tier,1), c.is_alive DESC, c.name
|
ORDER BY COALESCE(c.tier,1), c.is_alive DESC, c.name
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
alive_count = sum(1 for c in characters if c["is_alive"])
|
alive_count = sum(1 for c in characters if c["is_alive"])
|
||||||
total_count = len(characters)
|
|
||||||
|
|
||||||
events = query("""
|
events = query("""
|
||||||
SELECT e.*, c.name as actor_name
|
SELECT e.*, c.name as actor_name FROM events e
|
||||||
FROM events e
|
|
||||||
LEFT JOIN characters c ON c.id = e.actor_id
|
LEFT JOIN characters c ON c.id = e.actor_id
|
||||||
WHERE e.session_id = %s
|
WHERE e.session_id = %s
|
||||||
AND e.event_type NOT IN ('encounter_lambda','encounter_group','encounter_actif','encounter_boss')
|
AND e.event_type NOT IN ('encounter_lambda','encounter_group','encounter_actif','encounter_boss','cycle_tick')
|
||||||
ORDER BY e.day DESC, e.tick DESC, e.id DESC
|
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 50
|
||||||
LIMIT 50
|
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
encounters = query("""
|
encounters = query("""
|
||||||
SELECT e.*, c.name as actor_name
|
SELECT e.*, c.name as actor_name FROM events e
|
||||||
FROM events e
|
|
||||||
LEFT JOIN characters c ON c.id = e.actor_id
|
LEFT JOIN characters c ON c.id = e.actor_id
|
||||||
WHERE e.session_id = %s
|
WHERE e.session_id = %s AND e.event_type LIKE 'encounter%%'
|
||||||
AND e.event_type LIKE 'encounter%%'
|
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 30
|
||||||
ORDER BY e.day DESC, e.tick DESC, e.id DESC
|
|
||||||
LIMIT 30
|
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
world_state = query("""
|
world_state = query("""
|
||||||
SELECT DISTINCT ON (location_slug) *
|
SELECT DISTINCT ON (location_slug) *
|
||||||
FROM world_state
|
FROM world_state WHERE session_id = %s
|
||||||
WHERE session_id = %s
|
|
||||||
ORDER BY location_slug, day DESC
|
ORDER BY location_slug, day DESC
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
faction_relations = query("""
|
faction_relations = query("""
|
||||||
SELECT * FROM faction_relations
|
SELECT * FROM faction_relations WHERE session_id = %s
|
||||||
WHERE session_id = %s
|
|
||||||
ORDER BY relation_score ASC
|
ORDER BY relation_score ASC
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
@@ -368,33 +598,29 @@ def index():
|
|||||||
COUNT(*) FILTER (WHERE is_alive) as alive,
|
COUNT(*) FILTER (WHERE is_alive) as alive,
|
||||||
COUNT(*) FILTER (WHERE NOT is_alive) as dead
|
COUNT(*) FILTER (WHERE NOT is_alive) as dead
|
||||||
FROM characters WHERE session_id = %s
|
FROM characters WHERE session_id = %s
|
||||||
GROUP BY COALESCE(tier,1)
|
GROUP BY COALESCE(tier,1) ORDER BY COALESCE(tier,1)
|
||||||
ORDER BY COALESCE(tier,1)
|
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
tier_stats = [{"tier": r["tier"], "label": tier_labels.get(r["tier"],"?"),
|
tier_stats = [{"tier": r["tier"], "label": tier_labels.get(r["tier"],"?"),
|
||||||
"alive": r["alive"], "dead": r["dead"]} for r in tier_raw]
|
"alive": r["alive"], "dead": r["dead"]} for r in tier_raw]
|
||||||
|
|
||||||
zone_stats = query("""
|
zone_stats = query("""
|
||||||
SELECT location_slug, COUNT(*) as cnt
|
SELECT location_slug, COUNT(*) as cnt FROM characters
|
||||||
FROM characters
|
|
||||||
WHERE session_id = %s AND is_alive = TRUE
|
WHERE session_id = %s AND is_alive = TRUE
|
||||||
GROUP BY location_slug
|
GROUP BY location_slug ORDER BY cnt DESC LIMIT 5
|
||||||
ORDER BY cnt DESC
|
|
||||||
LIMIT 5
|
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
events_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,))
|
ev_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,))
|
||||||
combat_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,))
|
cb_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,))
|
||||||
deaths_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='death'", (sid,))
|
dt_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='death'", (sid,))
|
||||||
enc_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type LIKE 'encounter%%'", (sid,))
|
enc_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type LIKE 'encounter%%'", (sid,))
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"Événements total": events_total["n"] if events_total else 0,
|
"Événements total": ev_total["n"] if ev_total else 0,
|
||||||
"Combats": combat_total["n"] if combat_total else 0,
|
"Combats": cb_total["n"] if cb_total else 0,
|
||||||
"Morts": deaths_total["n"] if deaths_total else 0,
|
"Morts": dt_total["n"] if dt_total else 0,
|
||||||
"Rencontres": enc_total["n"] if enc_total else 0,
|
"Rencontres": enc_total["n"] if enc_total else 0,
|
||||||
"PNJ vivants": alive_count,
|
"PNJ vivants": alive_count,
|
||||||
"PNJ morts": total_count - alive_count,
|
"PNJ morts": len(characters) - alive_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
boss_inventory = query("""
|
boss_inventory = query("""
|
||||||
@@ -405,11 +631,11 @@ def index():
|
|||||||
ORDER BY c.name, i.is_equipped DESC
|
ORDER BY c.name, i.is_equipped DESC
|
||||||
""", (sid,))
|
""", (sid,))
|
||||||
|
|
||||||
return render_template_string(TEMPLATE,
|
return dict(
|
||||||
session=session,
|
session=session,
|
||||||
characters=characters,
|
characters=characters,
|
||||||
alive_count=alive_count,
|
alive_count=alive_count,
|
||||||
total_count=total_count,
|
total_count=len(characters),
|
||||||
events=events,
|
events=events,
|
||||||
encounters=encounters,
|
encounters=encounters,
|
||||||
world_state=world_state,
|
world_state=world_state,
|
||||||
@@ -420,11 +646,127 @@ def index():
|
|||||||
boss_inventory=boss_inventory,
|
boss_inventory=boss_inventory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
all_sessions = get_all_sessions()
|
||||||
|
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
|
||||||
|
data = get_sim_data(sid)
|
||||||
|
return render_template_string(TEMPLATE,
|
||||||
|
tab="sim",
|
||||||
|
current_sid=sid,
|
||||||
|
all_sessions=all_sessions,
|
||||||
|
flash_msg=None,
|
||||||
|
flash_ok=True,
|
||||||
|
modes={},
|
||||||
|
cfg={},
|
||||||
|
sim_configs_dir=SIM_CONFIGS_DIR,
|
||||||
|
**data,
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/params")
|
||||||
|
def params():
|
||||||
|
all_sessions = get_all_sessions()
|
||||||
|
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
|
||||||
|
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
|
||||||
|
cfg = load_sim_config(sid)
|
||||||
|
modes = cfg.get("modes", {})
|
||||||
|
flash_msg = request.args.get("msg")
|
||||||
|
flash_ok = request.args.get("ok", "1") == "1"
|
||||||
|
return render_template_string(TEMPLATE,
|
||||||
|
tab="params",
|
||||||
|
current_sid=sid,
|
||||||
|
all_sessions=all_sessions,
|
||||||
|
session=session,
|
||||||
|
cfg=cfg,
|
||||||
|
modes=modes,
|
||||||
|
sim_configs_dir=SIM_CONFIGS_DIR,
|
||||||
|
flash_msg=flash_msg,
|
||||||
|
flash_ok=flash_ok,
|
||||||
|
# unused in params tab but required by template
|
||||||
|
characters=[], alive_count=0, total_count=0,
|
||||||
|
events=[], encounters=[], world_state=[],
|
||||||
|
faction_relations=[], tier_stats=[], zone_stats=[],
|
||||||
|
stats={}, boss_inventory=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/params/save", methods=["POST"])
|
||||||
|
def params_save():
|
||||||
|
sid = int(request.args.get("sid", 1))
|
||||||
|
cfg = load_sim_config(sid)
|
||||||
|
if not cfg:
|
||||||
|
return redirect(url_for("params", sid=sid, msg="Fichier config introuvable", ok=0))
|
||||||
|
|
||||||
|
f = request.form
|
||||||
|
|
||||||
|
cfg["name"] = f.get("name", cfg.get("name", ""))
|
||||||
|
cfg["mode"] = f.get("mode", cfg.get("mode", "pacifiste"))
|
||||||
|
|
||||||
|
cfg.setdefault("tick", {})["tick_sleep_sec"] = int(f.get("tick_sleep_sec", 60))
|
||||||
|
|
||||||
|
cfg.setdefault("survival", {}).update({
|
||||||
|
"drain_speed": float(f.get("survival_drain_speed", 1.0)),
|
||||||
|
"fatigue_hp_per_2pts": int(f.get("survival_fatigue_hp", 2)),
|
||||||
|
"enabled": "survival_enabled" in f,
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg.setdefault("encounter", {}).update({
|
||||||
|
"global_rate_multiplier": float(f.get("encounter_rate", 1.0)),
|
||||||
|
"enabled": "encounter_enabled" in f,
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg.setdefault("economy", {})["caps_global_multiplier"] = float(f.get("caps_multiplier", 1.0))
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_sim_config(sid, cfg)
|
||||||
|
execute("UPDATE sessions SET mode=%s WHERE id=%s", (cfg["mode"], sid))
|
||||||
|
msg = f"Config sim_{sid:03d}.json sauvegardée. Mode DB mis à jour → {cfg['mode']}."
|
||||||
|
ok = 1
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Erreur: {e}"
|
||||||
|
ok = 0
|
||||||
|
|
||||||
|
return redirect(url_for("params", sid=sid, msg=msg, ok=ok))
|
||||||
|
|
||||||
|
@app.route("/params/setstatus", methods=["POST"])
|
||||||
|
def params_setstatus():
|
||||||
|
sid = int(request.args.get("sid", 1))
|
||||||
|
status = request.form.get("status", "paused")
|
||||||
|
try:
|
||||||
|
execute("UPDATE sessions SET status=%s WHERE id=%s", (status, sid))
|
||||||
|
msg = f"Session {sid} → status '{status}'. Le process run.py sortira au prochain tick si arrêté."
|
||||||
|
ok = 1
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Erreur: {e}"
|
||||||
|
ok = 0
|
||||||
|
return redirect(url_for("params", sid=sid, msg=msg, ok=ok))
|
||||||
|
|
||||||
|
@app.route("/params/resetday", methods=["POST"])
|
||||||
|
def params_resetday():
|
||||||
|
sid = int(request.args.get("sid", 1))
|
||||||
|
day = int(request.form.get("day", 1))
|
||||||
|
tick = int(request.form.get("tick", 0))
|
||||||
|
try:
|
||||||
|
execute("UPDATE sessions SET current_day=%s, current_tick=%s WHERE id=%s", (day, tick, sid))
|
||||||
|
msg = f"Session {sid} remise à J{day} T{tick}h."
|
||||||
|
ok = 1
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"Erreur: {e}"
|
||||||
|
ok = 0
|
||||||
|
return redirect(url_for("params", sid=sid, msg=msg, ok=ok))
|
||||||
|
|
||||||
@app.route("/api/state")
|
@app.route("/api/state")
|
||||||
def api_state():
|
def api_state():
|
||||||
session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
|
all_sessions = get_all_sessions()
|
||||||
sid = session["id"] if session else 1
|
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
|
||||||
alive = query("SELECT id, name, hp, max_hp, tier, faction_slug, location_slug FROM characters WHERE session_id=%s AND is_alive=TRUE", (sid,))
|
session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,))
|
||||||
|
alive = query("""
|
||||||
|
SELECT id, name, hp, max_hp, tier, faction_slug, location_slug
|
||||||
|
FROM characters WHERE session_id=%s AND is_alive=TRUE
|
||||||
|
""", (sid,))
|
||||||
return jsonify({"session": session, "alive_pnj": alive})
|
return jsonify({"session": session, "alive_pnj": alive})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user