Comportement SYSTÉMATIQUE à chaque création de jeu (admin compris), pas une formalité réservée à l'inscription : /onboarding (routes/onboarding/) devient le point d'entrée unique — 4 cartes retournables (survol = explication au dos), défilement horizontal animé vers le nom du jeu. Un admin y repasse à volonté (pas de project_slug dédié, jamais bloqué/ redirigé vers un projet précédent) ; un compte "user" n'en a plus qu'un créé d'office (routes/auth/register_2fa.py), guidé ici à la place. - db/games/game_type_catalog.py : catalogue des 4 types (Quiz/ Embranchement-escape game/RPG/Créer mon jeu de A à Z), _meta['onboarding_type'] décide du "kind" du premier écran créé et si le tableau de bord complet reste accessible. - routes/games/game_dashboard.py, templates/game_dashboard_simple.html : un type restreint (quiz/embranchement/rpg) voit désormais SON tableau de bord (même route que "custom"), rendu en version simplifiée — juste ses écrans en cartes avec un aperçu RÉEL du contenu (scène mise à l'échelle par container query CSS, adaptée à la largeur réelle de la carte). "+ Ajouter un écran" n'y propose pas de choix de type : imposé par le projet (routes/screens/screens_new.py), verrouillé aussi côté serveur. - core/auth_guard.py : plus de blocage de game_dashboard par type — la restriction se fait au rendu, pas à l'accès à la route. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
194 lines
11 KiB
HTML
194 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Forge Engine{% endblock %}</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='branding/forge-favicon.svg') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
<script>window.CSRF_TOKEN = {{ csrf_token()|tojson }};</script>
|
|
<script src="{{ url_for('static', filename='csrf_fetch.js') }}"></script>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="{% block body_class %}{% endblock %}">
|
|
<!-- #pageChrome : tout ce qui est en dehors de <main> mais doit quand même
|
|
changer d'une page à l'autre (barre du jeu comprise) — un seul bloc
|
|
stable pour que pjax.js puisse le remplacer d'un coup à chaque
|
|
navigation (voir swapDocument() dans pjax.js). Avant l'ajout de ce
|
|
conteneur, pjax.js cherchait un <header class="topbar"> qui n'a jamais
|
|
existé (la barre est un <nav>, pas un <header>) : la barre de
|
|
navigation du jeu (ajoutée après pjax.js) ne changeait donc jamais
|
|
pendant la navigation pjax — absente en arrivant sur un jeu tant qu'on
|
|
ne rechargeait pas la page en entier (Ctrl+F5), et inversement
|
|
laissée en place en revenant sur l'accueil (liste des jeux) où elle
|
|
n'a pourtant rien à faire. -->
|
|
<div id="pageChrome">
|
|
<nav class="navbar topbar" role="navigation" aria-label="main navigation">
|
|
<div class="navbar-brand">
|
|
<a href="{{ url_for('index') }}" class="navbar-item brand">
|
|
<img src="{{ url_for('static', filename='branding/forge-logo.svg') }}" alt="" width="32" height="32" style="border-radius:8px;">
|
|
<strong class="forge-gradient-title">Forge Engine</strong>
|
|
</a>
|
|
</div>
|
|
{% if g.get('current_user') %}
|
|
<div class="navbar-end" style="margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px;">
|
|
<a href="{{ url_for('profile') }}" class="hint" style="text-decoration:underline;">{{ g.current_user.prenom }} {{ g.current_user.nom }}{{ ' (admin)' if g.current_user.role == 'admin' else '' }}</a>
|
|
<form method="post" action="{{ url_for('logout') }}"><button type="submit" class="button is-small">Déconnexion</button></form>
|
|
</div>
|
|
{% endif %}
|
|
</nav>
|
|
{% if game is defined and game %}
|
|
<!-- Barre toujours visible entre les différentes pages d'UN MÊME jeu.
|
|
Écrans/Éléments de jeu/Variables/Objets n'ont plus leur propre page —
|
|
tout ça vit désormais dans les onglets du tableau de bord (voir
|
|
game_dashboard.html) — donc un seul lien y mène. Remplace le fil
|
|
d'Ariane (retiré, devenu redondant) : n'apparaît que si la route a mis
|
|
un `game` dans le contexte du template (déjà fait par toutes les
|
|
pages d'un jeu) — absente sur l'accueil (liste des jeux). "Jouer" est
|
|
délibérément le DERNIER lien — une action à part (ouvre l'aperçu
|
|
jouable dans un nouvel onglet), pas une page de gestion de plus. -->
|
|
<nav class="gameNavBar" aria-label="Navigation du jeu">
|
|
<!-- Onboarding guidé (voir routes/onboarding/onboarding_new.py) : même
|
|
route pour tous les comptes — routes/games/game_dashboard.py y rend
|
|
une version simplifiée (juste les écrans) pour un type "restreint"
|
|
(quiz/embranchement/rpg), la version complète pour "custom". -->
|
|
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="{{ 'Écrans du jeu' if game.onboarding_type in ('quiz', 'embranchement', 'rpg') else 'Écrans, objets, templates, variables' }}">📊 Tableau de bord</a>
|
|
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu (une seule partie partagée, pour la conception)">▶️ Jouer</a>
|
|
<button type="button" class="gameNavLink" onclick="window.forgeOpenPublishModal && window.forgeOpenPublishModal()" title="Exporte le jeu en exécutable Windows autonome (zip)">📦 Publier</button>
|
|
<!-- "Publier en ligne" (état par joueur, Phase 1) : distinct de
|
|
"Publier" ci-dessus (export .zip mono-joueur) — active/désactive la
|
|
route publique /jouer/<slug>, jouable par n'importe quel visiteur
|
|
anonyme avec SA PROPRE partie (voir routes/public_play/,
|
|
db/games/is_public_played.py). -->
|
|
<form method="post" action="{{ url_for('game_toggle_public_played', slug=game.slug) }}" style="display:inline;">
|
|
<input type="hidden" name="value" value="{{ '0' if game.is_public_played else '1' }}">
|
|
<button type="submit" class="gameNavLink" title="{{ 'Rendre le jeu privé (désactive /jouer)' if game.is_public_played else 'Rendre le jeu jouable par tous, chacun avec sa propre partie' }}">
|
|
{{ '🌐 En ligne ✓' if game.is_public_played else '🌐 Publier en ligne' }}
|
|
</button>
|
|
</form>
|
|
{% if game.is_public_played %}
|
|
<a href="{{ url_for('game_play_public', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Ouvre la partie publique, comme un vrai visiteur">🔗 Lien public</a>
|
|
{% endif %}
|
|
</nav>
|
|
{% endif %}
|
|
</div>
|
|
<main class="content {% block content_class %}{% endblock %}">
|
|
{% set recovery_codes = pop_recovery_codes() %}
|
|
{% if recovery_codes %}
|
|
<!-- À L'INTÉRIEUR de <main> (et pas à côté de #pageChrome) : pjax.js ne
|
|
remplace jamais que #pageChrome et main.content d'une navigation à
|
|
l'autre (voir swapDocument() dans pjax.js) — posé ailleurs, ce modal
|
|
serait bien présent dans le HTML renvoyé par le serveur juste après
|
|
l'inscription, mais jamais copié dans le DOM réellement affiché
|
|
puisque la redirection qui suit la confirmation de la 2FA passe par
|
|
une navigation pjax (fetch), pas un vrai rechargement de page. -->
|
|
<div class="modal is-active" id="recoveryCodesModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content">
|
|
<div class="box">
|
|
<h2 class="title is-4">Tes codes de récupération</h2>
|
|
<p class="hint mb-4">Note ces 10 codes en sécurité (gestionnaire de mots de passe, papier...) : chacun ne fonctionne qu'une seule fois et permet de te connecter si tu perds l'accès à ton application d'authentification. Ils ne seront plus jamais affichés.</p>
|
|
<div class="recoveryCodesGrid">
|
|
{% for code in recovery_codes %}
|
|
<code class="recoveryCode">{{ code }}</code>
|
|
{% endfor %}
|
|
</div>
|
|
<button type="button" class="button primary is-fullwidth mt-4" onclick="document.getElementById('recoveryCodesModal').remove()">J'ai noté mes codes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if game is defined and game %}
|
|
<!-- Même remarque que pour le modal des codes de récupération ci-dessus : à L'INTÉRIEUR
|
|
de <main> pour que pjax.js le remplace (et rejoue son script JS, voir
|
|
runScriptsIn()) à chaque navigation entre pages d'un même jeu — posé
|
|
dans #pageChrome (comme le bouton "📦 Publier" qui l'ouvre), son
|
|
script ne serait rejoué qu'au tout premier chargement complet de la
|
|
page. -->
|
|
<div class="modal" id="publishModal">
|
|
<div class="modal-background"></div>
|
|
<div class="modal-content">
|
|
<div class="box">
|
|
<h2 class="title is-4">Publier « {{ game.name }} »</h2>
|
|
<p class="hint mb-4">Construit un exécutable Windows autonome (Python + serveur embarqués) : dézippé puis lancé en double-cliquant sur <code>run.bat</code>, le jeu se joue hors connexion, sans rien installer.</p>
|
|
<div id="publishProgress">
|
|
<p class="hint" id="publishStepText">Préparation du jeu...</p>
|
|
<div class="passwordStrengthBar" style="margin-top:10px;"><div class="passwordStrengthFill strong" id="publishProgressFill" style="width:100%; animation:forgePublishPulse 1.2s ease-in-out infinite;"></div></div>
|
|
</div>
|
|
<div id="publishError" class="notification is-danger" style="display:none;"></div>
|
|
<div id="publishDone" class="notification is-success" style="display:none;">✅ Téléchargement lancé — dézippe le fichier puis double-clique sur <code>run.bat</code>.</div>
|
|
<div class="field mt-4" style="display:flex; justify-content:flex-end;">
|
|
<button type="button" class="button is-small" onclick="window.forgeClosePublishModal()">Fermer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>
|
|
@keyframes forgePublishPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
|
</style>
|
|
<script>
|
|
(function () {
|
|
var modal = document.getElementById('publishModal');
|
|
var stepText = document.getElementById('publishStepText');
|
|
var progressBox = document.getElementById('publishProgress');
|
|
var errorBox = document.getElementById('publishError');
|
|
var doneBox = document.getElementById('publishDone');
|
|
var steps = ["Préparation du jeu...", "Copie des données...", "Compression du dossier..."];
|
|
var stepTimer = null;
|
|
|
|
window.forgeOpenPublishModal = function () {
|
|
var stepIndex = 0;
|
|
stepText.textContent = steps[0];
|
|
progressBox.style.display = '';
|
|
errorBox.style.display = 'none';
|
|
doneBox.style.display = 'none';
|
|
modal.classList.add('is-active');
|
|
|
|
stepTimer = setInterval(function () {
|
|
stepIndex = (stepIndex + 1) % steps.length;
|
|
stepText.textContent = steps[stepIndex];
|
|
}, 1200);
|
|
|
|
var filename = "{{ game.slug }}.zip";
|
|
fetch("{{ url_for('publish_game', slug=game.slug) }}", { method: 'POST' })
|
|
.then(function (resp) {
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
var disposition = resp.headers.get('Content-Disposition') || '';
|
|
var match = /filename="?([^";]+)"?/.exec(disposition);
|
|
if (match) filename = match[1];
|
|
return resp.blob();
|
|
})
|
|
.then(function (blob) {
|
|
clearInterval(stepTimer);
|
|
progressBox.style.display = 'none';
|
|
doneBox.style.display = '';
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
})
|
|
.catch(function (err) {
|
|
clearInterval(stepTimer);
|
|
progressBox.style.display = 'none';
|
|
errorBox.style.display = '';
|
|
errorBox.textContent = "Échec de la publication (" + err.message + "). Réessaie, ou vérifie ta connexion internet (nécessaire uniquement lors de la toute première publication sur ce poste).";
|
|
});
|
|
};
|
|
|
|
window.forgeClosePublishModal = function () {
|
|
modal.classList.remove('is-active');
|
|
if (stepTimer) clearInterval(stepTimer);
|
|
};
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
<script src="{{ url_for('static', filename='pjax.js') }}"></script>
|
|
</body>
|
|
</html>
|