Export Web/SCORM : port complet du runtime jouable côté navigateur
Phase 1.2 de la feuille de route produit — un paquet SCORM tourne seul dans le LMS du client, sans serveur Forge Engine disponible. Ajoute static/js/play/offline/ (miroir JS de screens/rendering/ et data_actions/, sous window.FORGE_OFFLINE) pour que variables, score, données d'objet, répéteurs et conditions de visibilité fonctionnent entièrement en mémoire côté navigateur ; branche actions.js et bindings.js dessus au lieu d'un fetch() serveur. Ajoute publish/build_scorm_package.py (paquet statique + imsmanifest.xml SCORM 1.2 + wrapper API SCORM) et le bouton "Exporter (Web/SCORM)". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9776fbc057
commit
f3b72d73c9
@@ -55,6 +55,11 @@
|
||||
<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>
|
||||
<!-- Phase 1.2 (feuille de route produit) : paquet STATIQUE (aucun
|
||||
Python/Flask embarqué, voir publish/build_scorm_package.py), pensé
|
||||
pour un import dans le LMS du client (Moodle...) — distinct du
|
||||
zip exécutable ci-dessus, qui a besoin de Windows + double-clic. -->
|
||||
<button type="button" class="gameNavLink" onclick="window.forgeOpenScormModal && window.forgeOpenScormModal()" title="Exporte le jeu en paquet Web/SCORM importable dans un LMS">🎓 Exporter (Web/SCORM)</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
|
||||
@@ -185,6 +190,83 @@
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<div class="modal" id="scormModal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="box">
|
||||
<h2 class="title is-4">Exporter « {{ game.name }} » en Web/SCORM</h2>
|
||||
<p class="hint mb-4">Construit un paquet SCORM 1.2 (imsmanifest.xml + page web autonome, aucun serveur requis) : importe le .zip obtenu directement dans ton LMS (Moodle...) — le score et le statut de la partie remontent automatiquement.</p>
|
||||
<div id="scormProgress">
|
||||
<p class="hint" id="scormStepText">Préparation du jeu...</p>
|
||||
<div class="passwordStrengthBar" style="margin-top:10px;"><div class="passwordStrengthFill strong" id="scormProgressFill" style="width:100%; animation:forgePublishPulse 1.2s ease-in-out infinite;"></div></div>
|
||||
</div>
|
||||
<div id="scormError" class="notification is-danger" style="display:none;"></div>
|
||||
<div id="scormDone" class="notification is-success" style="display:none;">✅ Téléchargement lancé — importe directement le .zip dans ton LMS.</div>
|
||||
<div class="field mt-4" style="display:flex; justify-content:flex-end;">
|
||||
<button type="button" class="button is-small" onclick="window.forgeCloseScormModal()">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('scormModal');
|
||||
var stepText = document.getElementById('scormStepText');
|
||||
var progressBox = document.getElementById('scormProgress');
|
||||
var errorBox = document.getElementById('scormError');
|
||||
var doneBox = document.getElementById('scormDone');
|
||||
var steps = ["Préparation du jeu...", "Génération du paquet hors ligne...", "Écriture du manifeste SCORM..."];
|
||||
var stepTimer = null;
|
||||
|
||||
window.forgeOpenScormModal = 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 }}-scorm.zip";
|
||||
fetch("{{ url_for('export_scorm', 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 l'export (" + err.message + "). Réessaie.";
|
||||
});
|
||||
};
|
||||
|
||||
window.forgeCloseScormModal = function () {
|
||||
modal.classList.remove('is-active');
|
||||
if (stepTimer) clearInterval(stepTimer);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
+32
-11
@@ -4,12 +4,12 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ game.name }} — Jouer</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') }}">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset_url('branding/forge-favicon.svg') }}">
|
||||
<link rel="stylesheet" href="{{ asset_url('style.css') }}">
|
||||
<script>window.CSRF_TOKEN = {{ csrf_token()|tojson }};</script>
|
||||
<script src="{{ url_for('static', filename='csrf_fetch.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fonts/fonts.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/animate.min.css') }}"><style>
|
||||
<script src="{{ asset_url('csrf_fetch.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ asset_url('vendor/fonts/fonts.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset_url('vendor/animate.min.css') }}"><style>
|
||||
body{ margin:0; background:#000; }
|
||||
/* ---------- Zone de jeu à proportion fixe (voir #playFrame) ----------
|
||||
Chaque écran est conçu avec un format précis (Portrait/Paysage/Carré,
|
||||
@@ -113,6 +113,13 @@
|
||||
// distinctes (voir état par joueur, Phase 1).
|
||||
let gameData = {{ payload_json|safe }};
|
||||
let screensData = gameData.screens;
|
||||
{% if offline_mode %}
|
||||
// Export Web/SCORM (voir publish/build_scorm_package.py) : paquet
|
||||
// autonome sans serveur Forge Engine — actions.js/bindings.js basculent
|
||||
// sur static/js/play/offline/*.js (ci-dessous) au lieu d'un fetch().
|
||||
window.FORGE_OFFLINE = true;
|
||||
var FORGE_PLAY_URLS = {};
|
||||
{% else %}
|
||||
var FORGE_PLAY_URLS = {
|
||||
runtimePayload: {{ runtime_payload_url|tojson }},
|
||||
flowNodeRunData: {{ flow_node_run_data_url|tojson }},
|
||||
@@ -121,13 +128,27 @@
|
||||
flowNodeRunStatus: {{ flow_node_run_status_url|tojson }},
|
||||
flowNodeRunAddRow: {{ flow_node_run_add_row_url|tojson }},
|
||||
};
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/play/screens.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/play/conditions.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/play/actions.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/play/triggers.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/play/bindings.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/play/flow-engine.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/screens.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/conditions.js') }}"></script>
|
||||
{% if offline_mode %}
|
||||
<script src="{{ asset_url('js/play/offline/compute-operation.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/apply-actions.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/filter-repeater-rows.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/visibility-condition.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/render-jauge.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/render-overlay.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/render-special-widgets.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/render-repeater.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/render-element.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/offline/full-payload.js') }}"></script>
|
||||
{% if scorm_api_wrapper_url %}<script src="{{ scorm_api_wrapper_url }}"></script>{% endif %}
|
||||
{% endif %}
|
||||
<script src="{{ asset_url('js/play/actions.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/triggers.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/bindings.js') }}"></script>
|
||||
<script src="{{ asset_url('js/play/flow-engine.js') }}"></script>
|
||||
<script>
|
||||
// L'écran affiché au démarrage doit être choisi AVANT le premier
|
||||
// bindClicks() : la recherche du nœud Déclencheur associé à un élément se
|
||||
|
||||
Reference in New Issue
Block a user