From e82eb7ce8885b7088d32eaf633572c497604507d Mon Sep 17 00:00:00 2001 From: william Date: Wed, 2 Sep 2026 10:59:28 +0200 Subject: [PATCH] =?UTF-8?q?Retire=20l'export=20ex=C3=A9cutable=20(.exe)=20?= =?UTF-8?q?et=20la=20partie=20publique=20en=20ligne=20(/jouer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Décision produit : seul l'export Web/SCORM (LMS) est pertinent — l'export exécutable Windows autonome (publish/build_package.py, bouton "Publier") et la partie publique par joueur (/jouer/, routes/public_play/, "Publier en ligne") sont jugés redondants et retirés. Conserve le mécanisme d'état "par joueur" (db/global_vars, db/rows, per_player) : infrastructure générique déjà utilisée par Score/Progression et testée indépendamment de toute route publique (voir tests/test_player_state.py), aucune raison de la retirer. _STATIC_ITEMS/_copy_characters (copie sélective des sprites CraftPix réellement utilisés) migrent de build_package.py vers build_scorm_package.py, seul appelant restant. Co-Authored-By: Claude Sonnet 5 --- core/auth_guard.py | 11 -- core/player_identity.py | 35 ---- db/__init__.py | 4 +- db/constants.py | 10 +- db/games/game_meta.py | 6 - db/games/game_type_catalog.py | 2 +- db/games/get_game_type.py | 7 +- db/games/is_public_played.py | 14 -- db/games/set_public_played.py | 14 -- publish/__init__.py | 4 +- publish/build_package.py | 165 ------------------ publish/build_scorm_package.py | 67 ++++++- publish/player_app_template.py | 107 ------------ publish/run_bat_template.bat | 4 - publish/vendor_runtime.py | 91 ---------- routes/__init__.py | 2 +- routes/games/__init__.py | 2 +- routes/games/game_toggle_public_played.py | 16 -- routes/public_play/__init__.py | 9 - .../flow_node_run_add_row_public.py | 25 --- .../public_play/flow_node_run_data_public.py | 33 ---- .../public_play/flow_node_run_score_public.py | 25 --- .../flow_node_run_status_public.py | 25 --- .../flow_node_run_variable_public.py | 25 --- routes/public_play/game_play_public.py | 40 ----- routes/public_play/runtime_payload_public.py | 20 --- routes/publish/__init__.py | 2 +- routes/publish/publish_game.py | 28 --- screens/labels/animal_sprite_library.py | 2 +- screens/payload/full_game_payload.py | 27 +-- .../rendering/list_used_forge_characters.py | 2 +- templates/base.html | 109 ++---------- tests/test_add_row_action.py | 22 --- tests/test_animal_sprite_library.py | 9 +- tests/test_public_play.py | 116 ------------ tests/test_publish.py | 92 ---------- tests/test_scoring.py | 49 ++---- 37 files changed, 115 insertions(+), 1106 deletions(-) delete mode 100644 core/player_identity.py delete mode 100644 db/games/is_public_played.py delete mode 100644 db/games/set_public_played.py delete mode 100644 publish/build_package.py delete mode 100644 publish/player_app_template.py delete mode 100644 publish/run_bat_template.bat delete mode 100644 publish/vendor_runtime.py delete mode 100644 routes/games/game_toggle_public_played.py delete mode 100644 routes/public_play/__init__.py delete mode 100644 routes/public_play/flow_node_run_add_row_public.py delete mode 100644 routes/public_play/flow_node_run_data_public.py delete mode 100644 routes/public_play/flow_node_run_score_public.py delete mode 100644 routes/public_play/flow_node_run_status_public.py delete mode 100644 routes/public_play/flow_node_run_variable_public.py delete mode 100644 routes/public_play/game_play_public.py delete mode 100644 routes/public_play/runtime_payload_public.py delete mode 100644 routes/publish/publish_game.py delete mode 100644 tests/test_public_play.py delete mode 100644 tests/test_publish.py diff --git a/core/auth_guard.py b/core/auth_guard.py index 3a455682..add41e39 100644 --- a/core/auth_guard.py +++ b/core/auth_guard.py @@ -19,20 +19,9 @@ from .flask_app import app # Endpoints accessibles SANS être connecté — tout le reste exige une # session valide. "static" (CSS/JS/images) doit rester public : la page # de connexion elle-même en a besoin pour s'afficher. -# -# game_play_public/runtime_payload_public/flow_node_run_data_public/ -# flow_node_run_variable_public (routes/public_play/) : la route PUBLIQUE -# /jouer/, pour un vrai visiteur anonyme (état par joueur, Phase 1) -# — chaque vue vérifie ELLE-MÊME db.is_public_played(slug) (404 sinon), -# cette garde générique ne fait qu'autoriser l'accès sans connexion, elle -# ne dispense d'aucune autre vérification. _PUBLIC_ENDPOINTS = { "static", "login", "login_2fa", "register", "register_2fa", "logout", "forgot_password", "reset_password", - "game_play_public", "runtime_payload_public", - "flow_node_run_data_public", "flow_node_run_variable_public", - "flow_node_run_score_public", "flow_node_run_status_public", - "flow_node_run_add_row_public", } # Endpoints de gestion de COMPTE (routes/auth/profile.py) — connexion diff --git a/core/player_identity.py b/core/player_identity.py deleted file mode 100644 index d1639c71..00000000 --- a/core/player_identity.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Identité d'un visiteur anonyme de la route publique /jouer/ (voir -routes/public_play/) — état par joueur, Phase 1. Un cookie NON SIGNÉ (pas -besoin de itsdangerous/app.secret_key ici) : c'est une simple clé de -PARTITION des données (quelles lignes/valeurs appartiennent à ce visiteur), -jamais un jeton d'autorisation — la contrefaçon la plus grave possible -serait de se faire passer pour un AUTRE joueur anonyme du même jeu, pas de -franchir une frontière de sécurité (l'aperçu créateur, lui, reste protégé -par core/auth_guard.py, complètement indépendant de ce mécanisme).""" -import secrets - -from flask import request - -PLAYER_ID_COOKIE = "forge_player_id" -_PLAYER_ID_MAX_AGE = 60 * 60 * 24 * 365 # 1 an - - -def get_or_create_player_id(): - """Renvoie (player_id, is_new) — is_new indique s'il faut poser le - cookie sur la réponse avant de la renvoyer (voir - set_player_id_cookie ci-dessous) : à utiliser aussi bien depuis une - route qui rend une page (GET /jouer/) qu'une route API JSON - (runtime-payload, run-data, run-variable), dont la réponse n'est - construite qu'APRÈS avoir déjà besoin du player_id pour agir.""" - existing = request.cookies.get(PLAYER_ID_COOKIE) - if existing: - return existing, False - return secrets.token_urlsafe(16), True - - -def set_player_id_cookie(response, player_id): - response.set_cookie( - PLAYER_ID_COOKIE, player_id, - max_age=_PLAYER_ID_MAX_AGE, httponly=True, samesite="Lax", - ) - return response diff --git a/db/__init__.py b/db/__init__.py index e409d25e..8cab4a08 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -29,8 +29,6 @@ from .connection import connect from .games.list_games import list_games from .games.game_meta import game_meta -from .games.is_public_played import is_public_played -from .games.set_public_played import set_public_played from .games.get_game_type import get_game_type, DEFAULT_GAME_TYPE from .games.create_game import create_game from .games.update_game_name import update_game_name @@ -85,7 +83,7 @@ __all__ = [ "SCORE_STATUS_CHOICES", "SCORE_STATUS_LABELS", "get_score", "set_score_value", "set_status", "slugify", "table_name_for", "game_dir", "db_path", "connect", "list_games", "game_meta", "create_game", "update_game_name", "delete_game", "move_game", - "is_public_played", "set_public_played", "get_game_type", "DEFAULT_GAME_TYPE", + "get_game_type", "DEFAULT_GAME_TYPE", "ONBOARDING_TYPES", "DEFAULT_ONBOARDING_TYPE", "get_onboarding_type", "get_onboarding_type_raw", "set_onboarding_type", "is_restricted", "list_definitions", "get_definition", "create_definition", "rename_definition", diff --git a/db/constants.py b/db/constants.py index 6489623c..5eead8a7 100644 --- a/db/constants.py +++ b/db/constants.py @@ -3,13 +3,9 @@ import os # Surchargeable via FORGE_PROJECTS_DIR (même schéma que -# auth/connection.py::FORGE_USERS_DB_PATH) — utilisé par le serveur -# joueur autonome (publish/player_app_template.py), qui pose cette -# variable AVANT d'importer db/ pour pointer vers son propre dossier -# "projects/" embarqué plutôt que celui, réel, du poste de développement. -# Résolu ici, à l'IMPORT (pas par un appel de fonction) : la variable -# d'environnement doit donc déjà être posée avant le tout premier -# `import db`. +# auth/connection.py::FORGE_USERS_DB_PATH). Résolu ici, à l'IMPORT (pas +# par un appel de fonction) : la variable d'environnement doit donc déjà +# être posée avant le tout premier `import db`. PROJECTS_DIR = os.environ.get("FORGE_PROJECTS_DIR") or os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "projects" ) diff --git a/db/games/game_meta.py b/db/games/game_meta.py index 40eee84b..55bade7d 100644 --- a/db/games/game_meta.py +++ b/db/games/game_meta.py @@ -1,5 +1,4 @@ from ..connection import connect -from .is_public_played import is_public_played from .game_type_catalog import get_onboarding_type @@ -9,11 +8,6 @@ def game_meta(slug): conn.close() return { "slug": slug, "name": row["value"] if row else slug, - # État par joueur (Phase 1) : disponible partout où `game` est - # dans le contexte du template (base.html, la bascule "Publier en - # ligne" en a besoin pour afficher son état actuel), sans avoir à - # appeler is_public_played(slug) séparément dans chaque route. - "is_public_played": is_public_played(slug), # Onboarding guidé (voir game_type_catalog.py) : utilisé par # templates/base.html pour cacher le lien "Tableau de bord" à un # compte "restreint" (quiz/embranchement/rpg). diff --git a/db/games/game_type_catalog.py b/db/games/game_type_catalog.py index 438a9dd7..b4308b73 100644 --- a/db/games/game_type_catalog.py +++ b/db/games/game_type_catalog.py @@ -10,7 +10,7 @@ accessible ensuite, ou seulement sa version simplifiée (juste les écrans en cartes — voir routes/games/game_dashboard.py, restricted). _meta['onboarding_type'] (une ligne _meta par projet, même convention que -is_public_played/l'ancien game_type) retient CE choix — jamais lu par le +game_type) retient CE choix — jamais lu par le rendu jouable, seulement par le routage (routes/games/game_dashboard.py, routes/screens/screens_new.py).""" import db diff --git a/db/games/get_game_type.py b/db/games/get_game_type.py index ad1a1d1e..58162d1f 100644 --- a/db/games/get_game_type.py +++ b/db/games/get_game_type.py @@ -8,9 +8,10 @@ def get_game_type(slug): (éditeur générique actuel — quiz/formulaires/contenus, écrans = éléments HTML positionnés en %) ou "jeu_2d" (éditeur de scène dédié — voir screens/scenes/, écrans = objets pixel-positionnés en couches). - Réutilise _meta comme is_public_played — aucune nouvelle table. Un jeu - créé avant l'existence de cette clé (absente de _meta) retombe sur - "document", son comportement d'avant, inchangé.""" + Stocké dans _meta (une table générique clé/valeur, déjà utilisée pour + d'autres réglages de projet) — aucune nouvelle table. Un jeu créé avant + l'existence de cette clé (absente de _meta) retombe sur "document", + son comportement d'avant, inchangé.""" conn = connect(slug) row = conn.execute("SELECT value FROM _meta WHERE key = 'game_type'").fetchone() conn.close() diff --git a/db/games/is_public_played.py b/db/games/is_public_played.py deleted file mode 100644 index 32064302..00000000 --- a/db/games/is_public_played.py +++ /dev/null @@ -1,14 +0,0 @@ -from ..connection import connect - - -def is_public_played(slug): - """Un jeu n'est jouable sur la route publique /jouer/ (voir - routes/public_play/) que si le créateur l'a explicitement basculé - "Publier en ligne" dans son tableau de bord — jamais par défaut, pour - ne pas exposer un jeu en cours de conception. Réutilise la table - générique _meta (voir game_meta.py, déjà utilisée pour 'name'), - aucune nouvelle colonne/table nécessaire.""" - conn = connect(slug) - row = conn.execute("SELECT value FROM _meta WHERE key = 'is_public_played'").fetchone() - conn.close() - return bool(row and row["value"] == "1") diff --git a/db/games/set_public_played.py b/db/games/set_public_played.py deleted file mode 100644 index 2842e47f..00000000 --- a/db/games/set_public_played.py +++ /dev/null @@ -1,14 +0,0 @@ -from ..connection import connect - - -def set_public_played(slug, value): - """Bascule "Publier en ligne" (tableau de bord) — voir - is_public_played.py. `value` : bool.""" - conn = connect(slug) - conn.execute( - "INSERT INTO _meta (key, value) VALUES ('is_public_played', ?) " - "ON CONFLICT(key) DO UPDATE SET value = excluded.value", - ("1" if value else "0",), - ) - conn.commit() - conn.close() diff --git a/publish/__init__.py b/publish/__init__.py index 6be89863..3e889048 100644 --- a/publish/__init__.py +++ b/publish/__init__.py @@ -1,3 +1,3 @@ -"""publish — empaquette un jeu en exécutable Windows autonome (zip avec -serveur interne, voir build_package.py) : le bouton "Publier" de la barre +"""publish — empaquette un jeu en export Web/SCORM (voir +build_scorm_package.py) : le bouton "Exporter (Web/SCORM)" de la barre de navigation d'un jeu (templates/base.html).""" diff --git a/publish/build_package.py b/publish/build_package.py deleted file mode 100644 index ca385235..00000000 --- a/publish/build_package.py +++ /dev/null @@ -1,165 +0,0 @@ -"""Assemble le zip exporté d'un jeu (bouton "Publier", voir -routes/publish/publish_game.py) : un dossier autonome contenant Python -portable + Flask (vendorés une fois, voir vendor_runtime.py), une copie -figée du jeu (screens/db/filters/core minimal + templates/play.html + -le sous-ensemble utile de static/), sa base SQLite et ses fichiers -envoyés (uploads/), et les scripts de lancement (player_app.py/run.bat). - -Ne modifie JAMAIS les fichiers réels de l'app (tout se passe dans un -dossier de travail temporaire, supprimé après envoi de la réponse) ni la -vraie base de données du jeu (copiée, jamais déplacée).""" -import os -import shutil -import tempfile -import zipfile - -import db -import screens - -from . import vendor_runtime - -_PUBLISH_DIR = os.path.dirname(os.path.abspath(__file__)) -_BASE_DIR = os.path.dirname(_PUBLISH_DIR) - -# Sous-ensemble de static/ réellement nécessaire au mode jouable (voir -# templates/play.html) — jamais les assets propres à l'éditeur seul -# (icônes de l'interface de l'éditeur mises à part, déjà dans icons/). -_STATIC_ITEMS = ["style.css", "csrf_fetch.js", "icons", "branding", "vendor/fonts", "vendor/animate.min.css", "js/play", "characters"] - -_SLUG_PLACEHOLDER = "_SLUG_PLACEHOLDER" - - -def _copy_characters(static_dir, used_forge_characters): - """`static/characters/` contient 2 rangements bien distincts (voir le - plan "Bibliothèque de sprites animaux CraftPix") : le rangement plat - Kenney (`/*.png`, CC0, ~6 Mo, toujours copié comme avant) et - `animals///` (CraftPix, ~1,5-2 Go pour les 210 variantes) - — celui-ci copié SÉLECTIVEMENT, seulement les dossiers (animal, NN) - correspondant aux personnages `animal--` réellement - utilisés par CE jeu (used_forge_characters), pour ne pas gonfler - CHAQUE .zip exporté du poids de toute la bibliothèque. manifest.json - est toujours copié : screens/labels/animal_sprite_library.py le lit à - l'import, sans lui `screens` (copié intégralement juste après) ne - s'importerait plus dans le paquet exporté.""" - src_root = os.path.join(_BASE_DIR, "static", "characters") - dst_root = os.path.join(static_dir, "characters") - for name in os.listdir(src_root): - if name == "animals": - continue - src = os.path.join(src_root, name) - dst = os.path.join(dst_root, name) - if os.path.isdir(src): - shutil.copytree(src, dst) - else: - os.makedirs(dst_root, exist_ok=True) - shutil.copy2(src, dst) - - animals_src_root = os.path.join(src_root, "animals") - if not os.path.isdir(animals_src_root): - return - animals_dst_root = os.path.join(dst_root, "animals") - os.makedirs(animals_dst_root, exist_ok=True) - shutil.copy2(os.path.join(animals_src_root, "manifest.json"), os.path.join(animals_dst_root, "manifest.json")) - used_variants = set() - for slug in used_forge_characters: - # slug = "animal--" (voir screens/labels/animal_sprite_library.py) ; - # ignore silencieusement tout ce qui ne colle pas à ce format — - # un personnage Kenney (déjà copié ci-dessus) n'a pas ce préfixe. - if not slug.startswith("animal-"): - continue - animal, _, nn = slug[len("animal-"):].rpartition("-") - if animal: - used_variants.add((animal, nn)) - for animal, nn in used_variants: - src = os.path.join(animals_src_root, animal, nn) - if os.path.isdir(src): - shutil.copytree(src, os.path.join(animals_dst_root, animal, nn)) - - -def _copy_engine_sources(staging_dir, slug): - """Copie la partie du moteur nécessaire au mode jouable SEUL (jamais - l'éditeur ni l'authentification — voir le constat d'exploration dans - le plan : screens/ et db/ sont déjà totalement découplés de auth// - routes/, core/flask_app.py et core/jinja_filters.py sont les 2 SEULS - fichiers de core/ dont ils dépendent réellement).""" - for pkg in ("screens", "db", "filters"): - shutil.copytree(os.path.join(_BASE_DIR, pkg), os.path.join(staging_dir, pkg), - ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - - core_dir = os.path.join(staging_dir, "core") - os.makedirs(core_dir, exist_ok=True) - for fname in ("__init__.py", "flask_app.py", "jinja_filters.py"): - shutil.copy2(os.path.join(_BASE_DIR, "core", fname), os.path.join(core_dir, fname)) - - templates_dir = os.path.join(staging_dir, "templates") - os.makedirs(templates_dir, exist_ok=True) - shutil.copy2(os.path.join(_BASE_DIR, "templates", "play.html"), os.path.join(templates_dir, "play.html")) - - static_dir = os.path.join(staging_dir, "static") - used_forge_characters = screens.list_used_forge_characters(slug) - for item in _STATIC_ITEMS: - if item == "characters": - _copy_characters(static_dir, used_forge_characters) - continue - src = os.path.join(_BASE_DIR, "static", item) - dst = os.path.join(static_dir, item) - os.makedirs(os.path.dirname(dst), exist_ok=True) - if os.path.isdir(src): - shutil.copytree(src, dst) - else: - shutil.copy2(src, dst) - - -def _copy_game_data(staging_dir, slug): - """Copie le dossier RÉEL du jeu (game.db + uploads/, voir - db.game_dir) tel quel — jamais déplacé, jamais modifié.""" - src = db.game_dir(slug) - dst = os.path.join(staging_dir, "projects", slug) - shutil.copytree(src, dst, ignore=shutil.ignore_patterns("index.html", "index.css", "index.js")) - - -def _write_player_app(staging_dir, slug): - template_path = os.path.join(_PUBLISH_DIR, "player_app_template.py") - with open(template_path, encoding="utf-8") as f: - content = f.read() - content = content.replace(_SLUG_PLACEHOLDER, slug) - with open(os.path.join(staging_dir, "player_app.py"), "w", encoding="utf-8") as f: - f.write(content) - - -def _write_run_bat(staging_dir): - src = os.path.join(_PUBLISH_DIR, "run_bat_template.bat") - shutil.copy2(src, os.path.join(staging_dir, "run.bat")) - - -def _zip_dir(staging_dir, zip_path): - with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: - for root, _dirs, files in os.walk(staging_dir): - for name in files: - full = os.path.join(root, name) - rel = os.path.relpath(full, staging_dir) - zf.write(full, rel) - - -def build_game_zip(slug): - """Construit le zip et renvoie son chemin (dans un dossier temporaire - — à l'appelant de le supprimer une fois la réponse HTTP envoyée, voir - routes/publish/publish_game.py).""" - game = db.game_meta(slug) - python_dir, pylibs_dir = vendor_runtime.ensure_vendor_ready() - - staging_dir = tempfile.mkdtemp(prefix="forge_publish_") - try: - shutil.copytree(python_dir, os.path.join(staging_dir, "python-embed")) - shutil.copytree(pylibs_dir, os.path.join(staging_dir, "python-embed", "pylibs")) - _copy_engine_sources(staging_dir, slug) - _copy_game_data(staging_dir, slug) - _write_player_app(staging_dir, slug) - _write_run_bat(staging_dir) - - zip_fd, zip_path = tempfile.mkstemp(prefix="forge_publish_", suffix=".zip") - os.close(zip_fd) - _zip_dir(staging_dir, zip_path) - finally: - shutil.rmtree(staging_dir, ignore_errors=True) - return zip_path, game["name"] diff --git a/publish/build_scorm_package.py b/publish/build_scorm_package.py index 2edbd65f..9ec97f09 100644 --- a/publish/build_scorm_package.py +++ b/publish/build_scorm_package.py @@ -1,12 +1,8 @@ """Assemble le zip Web/SCORM d'un jeu (bouton "Exporter (Web/SCORM)", voir -routes/publish/export_scorm.py) : à la différence de build_package.py -(qui embarque Python + Flask, voir player_app.py), ce paquet est -STATIQUE — index.html porte tout le runtime jouable directement en -mémoire (window.FORGE_OFFLINE, voir static/js/play/offline/) puisqu'un -SCORM tourne SEUL dans le LMS du client, sans serveur Forge Engine -disponible. Rien de commun avec le zip .exe hors le sous-ensemble de -static/ et _copy_characters (mêmes assets, mêmes règles de copie -sélective) — réutilisés depuis build_package plutôt que dupliqués.""" +routes/publish/export_scorm.py) — paquet STATIQUE : index.html porte tout +le runtime jouable directement en mémoire (window.FORGE_OFFLINE, voir +static/js/play/offline/) puisqu'un SCORM tourne SEUL dans le LMS du +client, sans serveur Forge Engine disponible.""" import base64 import json import os @@ -19,12 +15,65 @@ from flask import render_template import db import screens -from .build_package import _copy_characters, _STATIC_ITEMS from .scorm_manifest import build_manifest_xml _PUBLISH_DIR = os.path.dirname(os.path.abspath(__file__)) _BASE_DIR = os.path.dirname(_PUBLISH_DIR) +# Sous-ensemble de static/ réellement nécessaire au mode jouable (voir +# templates/play.html) — jamais les assets propres à l'éditeur seul +# (icônes de l'interface de l'éditeur mises à part, déjà dans icons/). +_STATIC_ITEMS = ["style.css", "csrf_fetch.js", "icons", "branding", "vendor/fonts", "vendor/animate.min.css", "js/play", "characters"] + + +def _copy_characters(static_dir, used_forge_characters): + """`static/characters/` contient 2 rangements bien distincts (voir le + plan "Bibliothèque de sprites animaux CraftPix") : le rangement plat + Kenney (`/*.png`, CC0, ~6 Mo, toujours copié comme avant) et + `animals///` (CraftPix, ~1,5-2 Go pour les 210 variantes) + — celui-ci copié SÉLECTIVEMENT, seulement les dossiers (animal, NN) + correspondant aux personnages `animal--` réellement + utilisés par CE jeu (used_forge_characters), pour ne pas gonfler + CHAQUE .zip exporté du poids de toute la bibliothèque. manifest.json + est toujours copié : screens/labels/animal_sprite_library.py le lit à + l'import, sans lui `screens` (copié intégralement juste après) ne + s'importerait plus dans le paquet exporté.""" + src_root = os.path.join(_BASE_DIR, "static", "characters") + dst_root = os.path.join(static_dir, "characters") + for name in os.listdir(src_root): + if name == "animals": + continue + src = os.path.join(src_root, name) + dst = os.path.join(dst_root, name) + if os.path.isdir(src): + shutil.copytree(src, dst) + else: + os.makedirs(dst_root, exist_ok=True) + shutil.copy2(src, dst) + + animals_src_root = os.path.join(src_root, "animals") + if not os.path.isdir(animals_src_root): + return + animals_dst_root = os.path.join(dst_root, "animals") + os.makedirs(animals_dst_root, exist_ok=True) + shutil.copy2(os.path.join(animals_src_root, "manifest.json"), os.path.join(animals_dst_root, "manifest.json")) + used_variants = set() + for character_slug in used_forge_characters: + # character_slug = "animal--" (voir + # screens/labels/animal_sprite_library.py) ; ignore silencieusement + # tout ce qui ne colle pas à ce format — un personnage Kenney + # (déjà copié ci-dessus) n'a pas ce préfixe. + if not character_slug.startswith("animal-"): + continue + animal, _, nn = character_slug[len("animal-"):].rpartition("-") + if animal: + used_variants.add((animal, nn)) + for animal, nn in used_variants: + src = os.path.join(animals_src_root, animal, nn) + if os.path.isdir(src): + shutil.copytree(src, os.path.join(animals_dst_root, animal, nn)) + + def _copy_static_assets(staging_dir, slug): static_dir = os.path.join(staging_dir, "static") used_forge_characters = screens.list_used_forge_characters(slug) diff --git a/publish/player_app_template.py b/publish/player_app_template.py deleted file mode 100644 index c4e976dc..00000000 --- a/publish/player_app_template.py +++ /dev/null @@ -1,107 +0,0 @@ -"""player_app.py — serveur autonome généré par Forge Engine pour UN SEUL -jeu (voir publish/build_package.py) : aucune installation externe requise, -lancé par double-clic sur run.bat (Python portable + Flask embarqués à -côté de ce fichier). Reprend telles quelles les routes utilisées par le -mode jouable (voir templates/play.html) — jamais l'éditeur ni -l'authentification, absents de cet export. - -SLUG est remplacé littéralement par build_package.py au moment de la -publication (voir _SLUG_PLACEHOLDER) — ce fichier n'est jamais exécuté -tel quel depuis le dépôt source.""" -import json -import os -import socket -import sys -import threading -import webbrowser - -_BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - -# Le python311._pth de Python embeddable (voir publish/vendor_runtime.py) -# ne met sur sys.path QUE le dossier de python.exe lui-même (python-embed/) -# et "pylibs" — jamais le dossier de CE script, qui vit un cran au-dessus -# (sibling de python-embed/). Sans cette ligne, "import core"/"db"/ -# "screens" échoue avec ModuleNotFoundError bien qu'ils soient juste à -# côté de ce fichier. -sys.path.insert(0, _BASE_DIR) - -# Doit être posé AVANT tout import de db/ : db/constants.py lit -# FORGE_PROJECTS_DIR à l'IMPORT, pas à l'appel — pointe ici vers le -# dossier "projects/" embarqué à côté de ce fichier, jamais un chemin du -# poste où le jeu a été publié. -os.environ["FORGE_PROJECTS_DIR"] = os.path.join(_BASE_DIR, "projects") - -from flask import jsonify, redirect, render_template, request, send_from_directory, url_for # noqa: E402 - -from core.flask_app import app # noqa: E402 -from core import jinja_filters # noqa: E402,F401 - enregistre les filtres Jinja (elstyle/eltransform/elabel/colname) -import db # noqa: E402 -import screens # noqa: E402 - -# Pas de CSRF/auth dans cet export (un seul jeu, aucune connexion) — -# play.html appelle quand même csrf_token() en Jinja (voir base.html/ -# play.html) : un no-op le laisse fonctionner tel quel, sans le forker. -app.jinja_env.globals["csrf_token"] = lambda: "" - -SLUG = "_SLUG_PLACEHOLDER" - - -@app.route("/") -def index(): - return redirect(url_for("game_play", slug=SLUG)) - - -@app.route("/game//play") -def game_play(slug): - game = db.game_meta(SLUG) - payload = screens.full_game_payload(SLUG) - return render_template( - "play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload), - ) - - -@app.route("/game//runtime-payload") -def runtime_payload(slug): - return jsonify(screens.full_game_payload(SLUG)) - - -@app.route("/game//flow/nodes//run-data", methods=["POST"]) -def flow_node_run_data(slug, node_id): - node = screens.get_flow_node(SLUG, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee": - return jsonify({"ok": False}), 400 - if node.get("target_row_id") == screens.CLICKED_ROW_ID: - body = request.get_json(silent=True) or {} - node = {**node, "target_row_id": body.get("clicked_row_id")} - ok = screens.apply_data_action(SLUG, node) - return jsonify({"ok": ok}) - - -@app.route("/game//flow/nodes//run-variable", methods=["POST"]) -def flow_node_run_variable(slug, node_id): - node = screens.get_flow_node(SLUG, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_variable": - return jsonify({"ok": False}), 400 - ok = screens.apply_variable_action(SLUG, node) - return jsonify({"ok": ok}) - - -@app.route("/game//uploads/") -def uploaded_file(slug, filename): - return send_from_directory(os.path.join(db.game_dir(SLUG), "uploads"), filename) - - -def _open_browser(port): - webbrowser.open(f"http://127.0.0.1:{port}/") - - -if __name__ == "__main__": - # Port libre choisi au hasard par l'OS (jamais le même serveur à - # relancer sur un port déjà pris par une partie précédente encore - # ouverte) — le navigateur est ouvert automatiquement, l'utilisateur - # n'a jamais besoin de connaître ni taper le port. - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("127.0.0.1", 0)) - port = s.getsockname()[1] - threading.Timer(1.0, _open_browser, args=(port,)).start() - app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False) diff --git a/publish/run_bat_template.bat b/publish/run_bat_template.bat deleted file mode 100644 index 660f8d43..00000000 --- a/publish/run_bat_template.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -cd /d "%~dp0" -python-embed\python.exe player_app.py -pause diff --git a/publish/vendor_runtime.py b/publish/vendor_runtime.py deleted file mode 100644 index 92824afa..00000000 --- a/publish/vendor_runtime.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Prépare, une seule fois par poste, tout ce qu'il faut pour empaqueter -un jeu en exécutable autonome : Python portable (embeddable, python.org) -et les paquets pip nécessaires (Flask + ses dépendances), mis en cache -localement sous data/publish_vendor/ (non commité, voir .gitignore — -même dossier que data/secret_key/data/users.db, déjà runtime-only). - -La toute PREMIÈRE publication d'un jeu sur un poste donné télécharge ces -~15 Mo (connexion internet nécessaire, une seule fois) ; toutes les -publications suivantes réutilisent le cache, hors-ligne. Le JOUEUR qui -reçoit le zip final, lui, n'a jamais besoin d'internet — ce cache ne -concerne que la machine qui PUBLIE. - -Fonctions séparées et mockables (download_python_embeddable/ -vendor_flask) pour que les tests ne déclenchent jamais un vrai -téléchargement.""" -import os -import subprocess -import sys -import urllib.request -import zipfile - -_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -VENDOR_DIR = os.environ.get("FORGE_PUBLISH_VENDOR_DIR") or os.path.join(_BASE_DIR, "data", "publish_vendor") - -PYTHON_VERSION = "3.11.9" -PYTHON_EMBED_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip" -PYTHON_EMBED_DIR = os.path.join(VENDOR_DIR, "python-embed-amd64") -PYLIBS_DIR = os.path.join(VENDOR_DIR, "pylibs") - -# Mêmes versions que requirements.txt — seul Flask est listé explicitement, -# ses dépendances (Werkzeug/Jinja2/MarkupSafe/click/itsdangerous/blinker) -# suivent automatiquement via pip. -FLASK_SPEC = "Flask==3.0.3" - - -def download_python_embeddable(): - """Télécharge et extrait le ZIP Python embeddable officiel si absent - du cache — no-op si déjà présent (idempotent, sûr à rappeler à - chaque publication).""" - marker = os.path.join(PYTHON_EMBED_DIR, "python.exe") - if os.path.isfile(marker): - return PYTHON_EMBED_DIR - os.makedirs(PYTHON_EMBED_DIR, exist_ok=True) - zip_path = os.path.join(VENDOR_DIR, "python-embed-amd64.zip") - urllib.request.urlretrieve(PYTHON_EMBED_URL, zip_path) - with zipfile.ZipFile(zip_path) as zf: - zf.extractall(PYTHON_EMBED_DIR) - os.remove(zip_path) - _enable_pylibs_path(PYTHON_EMBED_DIR) - return PYTHON_EMBED_DIR - - -def _enable_pylibs_path(python_embed_dir): - """Le ZIP embeddable désactive site-packages par défaut - (python311._pth ne contient que "python311.zip" et "."). On ajoute une - ligne "pylibs" (chemin relatif à python.exe) plutôt que de réactiver - tout le mécanisme "site" — les paquets vendorés (Flask...) sont de - simples dossiers purement Python, aucun besoin d'aller plus loin.""" - pth_files = [f for f in os.listdir(python_embed_dir) if f.endswith("._pth")] - if not pth_files: - return - pth_path = os.path.join(python_embed_dir, pth_files[0]) - with open(pth_path, encoding="utf-8") as f: - content = f.read() - if "pylibs" not in content: - with open(pth_path, "a", encoding="utf-8") as f: - f.write("\npylibs\n") - - -def vendor_flask(): - """pip install --target=... : installe Flask (paquets purs Python, - indépendants de la version exacte de l'interpréteur embeddable) dans - le cache, réutilisé par tous les exports suivants — no-op si déjà - fait.""" - if os.path.isdir(PYLIBS_DIR) and os.listdir(PYLIBS_DIR): - return PYLIBS_DIR - os.makedirs(PYLIBS_DIR, exist_ok=True) - subprocess.run( - [sys.executable, "-m", "pip", "install", "--target", PYLIBS_DIR, "--no-compile", FLASK_SPEC], - check=True, - ) - return PYLIBS_DIR - - -def ensure_vendor_ready(): - """Point d'entrée unique appelé par build_package.py — télécharge/ - installe ce qui manque, puis renvoie (dossier_python_embed, - dossier_pylibs).""" - python_dir = download_python_embeddable() - pylibs_dir = vendor_flask() - return python_dir, pylibs_dir diff --git a/routes/__init__.py b/routes/__init__.py index 004bb4e4..13e82f50 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -1 +1 @@ -from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks, public_play, scenes, onboarding +from . import auth, games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars, publish, custom_events, flow_blocks, scenes, onboarding diff --git a/routes/games/__init__.py b/routes/games/__init__.py index 2d18d699..c7e6c026 100644 --- a/routes/games/__init__.py +++ b/routes/games/__init__.py @@ -1 +1 @@ -from . import index, games_new, game_dashboard, game_rename, game_delete, game_toggle_public_played, game_scoring_view +from . import index, games_new, game_dashboard, game_rename, game_delete, game_scoring_view diff --git a/routes/games/game_toggle_public_played.py b/routes/games/game_toggle_public_played.py deleted file mode 100644 index a50e7d5b..00000000 --- a/routes/games/game_toggle_public_played.py +++ /dev/null @@ -1,16 +0,0 @@ -from flask import request, redirect, url_for - -import db - -from core.flask_app import app - - -@app.route("/game//toggle-public-played", methods=["POST"]) -def game_toggle_public_played(slug): - """Bascule "Publier en ligne" (voir db/games/is_public_played.py) — - active/désactive la route publique /jouer/ (routes/public_play/), - jouable par n'importe quel visiteur anonyme avec sa propre partie - (état par joueur, Phase 1). Distinct de "Publier" (export .zip - exécutable mono-joueur, voir routes/publish/publish_game.py).""" - db.set_public_played(slug, request.form.get("value") == "1") - return redirect(request.referrer or url_for("game_dashboard", slug=slug)) diff --git a/routes/public_play/__init__.py b/routes/public_play/__init__.py deleted file mode 100644 index 03b1db91..00000000 --- a/routes/public_play/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from . import ( - game_play_public, - runtime_payload_public, - flow_node_run_data_public, - flow_node_run_variable_public, - flow_node_run_score_public, - flow_node_run_status_public, - flow_node_run_add_row_public, -) diff --git a/routes/public_play/flow_node_run_add_row_public.py b/routes/public_play/flow_node_run_add_row_public.py deleted file mode 100644 index 1f4ae06d..00000000 --- a/routes/public_play/flow_node_run_add_row_public.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import jsonify, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//flow/nodes//run-add-row", methods=["POST"]) -def flow_node_run_add_row_public(slug, node_id): - """Miroir public de routes/flow/flow_node_run_add_row.py (aperçu - créateur) — voir routes/public_play/game_play_public.py pour le - principe général.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - node = screens.get_flow_node(slug, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "ajouter_ligne": - return jsonify({"ok": False}), 400 - row_id = screens.apply_add_row_action(slug, node, player_id) - resp = app.make_response(jsonify({"ok": row_id is not None, "row_id": row_id})) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/flow_node_run_data_public.py b/routes/public_play/flow_node_run_data_public.py deleted file mode 100644 index 605bac16..00000000 --- a/routes/public_play/flow_node_run_data_public.py +++ /dev/null @@ -1,33 +0,0 @@ -from flask import jsonify, request, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//flow/nodes//run-data", methods=["POST"]) -def flow_node_run_data_public(slug, node_id): - """Miroir public de routes/flow/flow_node_run_data.py (aperçu créateur) - — voir routes/public_play/game_play_public.py pour le principe - général. `player_id` (le cookie du visiteur) est passé à - apply_data_action pour qu'un objet per_player agisse sur SA ligne.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - node = screens.get_flow_node(slug, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee": - return jsonify({"ok": False}), 400 - if node.get("target_row_id") == screens.CLICKED_ROW_ID: - body = request.get_json(silent=True) or {} - node = {**node, "target_row_id": body.get("clicked_row_id")} - elif node.get("target_row_id") == screens.LAST_INSERTED_ROW_ID: - # Phase 4 — voir routes/flow/flow_node_run_data.py. - body = request.get_json(silent=True) or {} - node = {**node, "target_row_id": body.get("last_inserted_row_id")} - ok = screens.apply_data_action(slug, node, player_id) - resp = app.make_response(jsonify({"ok": ok})) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/flow_node_run_score_public.py b/routes/public_play/flow_node_run_score_public.py deleted file mode 100644 index 5c337186..00000000 --- a/routes/public_play/flow_node_run_score_public.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import jsonify, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//flow/nodes//run-score", methods=["POST"]) -def flow_node_run_score_public(slug, node_id): - """Miroir public de routes/flow/flow_node_run_score.py (aperçu - créateur) — voir routes/public_play/game_play_public.py pour le - principe général.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - node = screens.get_flow_node(slug, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_score": - return jsonify({"ok": False}), 400 - ok = screens.apply_score_action(slug, node, player_id) - resp = app.make_response(jsonify({"ok": ok})) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/flow_node_run_status_public.py b/routes/public_play/flow_node_run_status_public.py deleted file mode 100644 index c6238e0a..00000000 --- a/routes/public_play/flow_node_run_status_public.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import jsonify, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//flow/nodes//run-status", methods=["POST"]) -def flow_node_run_status_public(slug, node_id): - """Miroir public de routes/flow/flow_node_run_status.py (aperçu - créateur) — voir routes/public_play/game_play_public.py pour le - principe général.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - node = screens.get_flow_node(slug, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "definir_statut_partie": - return jsonify({"ok": False}), 400 - ok = screens.apply_status_action(slug, node, player_id) - resp = app.make_response(jsonify({"ok": ok})) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/flow_node_run_variable_public.py b/routes/public_play/flow_node_run_variable_public.py deleted file mode 100644 index cb0ad632..00000000 --- a/routes/public_play/flow_node_run_variable_public.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import jsonify, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//flow/nodes//run-variable", methods=["POST"]) -def flow_node_run_variable_public(slug, node_id): - """Miroir public de routes/flow/flow_node_run_variable.py (aperçu - créateur) — voir routes/public_play/game_play_public.py pour le - principe général.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - node = screens.get_flow_node(slug, node_id) - if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_variable": - return jsonify({"ok": False}), 400 - ok = screens.apply_variable_action(slug, node, player_id) - resp = app.make_response(jsonify({"ok": ok})) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/game_play_public.py b/routes/public_play/game_play_public.py deleted file mode 100644 index e4ea9a55..00000000 --- a/routes/public_play/game_play_public.py +++ /dev/null @@ -1,40 +0,0 @@ -import json - -from flask import render_template, url_for, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer/") -def game_play_public(slug): - """Route PUBLIQUE, non authentifiée (voir core/auth_guard.py) — - distincte de /game//play (aperçu créateur, connecté, INCHANGÉE) : - un vrai visiteur anonyme, identifié par un cookie (voir - core/player_identity.py), avec sa propre partie (variables/objets - per_player, voir db/global_vars/, db/rows/). 404 si le créateur n'a - pas explicitement basculé "Publier en ligne" dans son tableau de bord - — jamais exposé par défaut.""" - if not db.is_public_played(slug): - abort(404) - game = db.game_meta(slug) - player_id, is_new = get_or_create_player_id() - payload = screens.full_game_payload(slug, player_id) - resp = app.make_response( - render_template( - "play.html", game=game, screens_data=payload["screens"], payload_json=json.dumps(payload), - asset_url=lambda filename: url_for("static", filename=filename), - runtime_payload_url=url_for("runtime_payload_public", slug=slug), - flow_node_run_data_url=url_for("flow_node_run_data_public", slug=slug, node_id=999999), - flow_node_run_variable_url=url_for("flow_node_run_variable_public", slug=slug, node_id=999999), - flow_node_run_score_url=url_for("flow_node_run_score_public", slug=slug, node_id=999999), - flow_node_run_status_url=url_for("flow_node_run_status_public", slug=slug, node_id=999999), - flow_node_run_add_row_url=url_for("flow_node_run_add_row_public", slug=slug, node_id=999999), - ) - ) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/public_play/runtime_payload_public.py b/routes/public_play/runtime_payload_public.py deleted file mode 100644 index 867706ea..00000000 --- a/routes/public_play/runtime_payload_public.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask import jsonify, abort - -import db -import screens - -from core.flask_app import app -from core.player_identity import get_or_create_player_id, set_player_id_cookie - - -@app.route("/jouer//runtime-payload") -def runtime_payload_public(slug): - """Miroir public de routes/flow/runtime_payload.py (aperçu créateur) — - voir routes/public_play/game_play_public.py pour le principe général.""" - if not db.is_public_played(slug): - abort(404) - player_id, is_new = get_or_create_player_id() - resp = app.make_response(jsonify(screens.full_game_payload(slug, player_id))) - if is_new: - set_player_id_cookie(resp, player_id) - return resp diff --git a/routes/publish/__init__.py b/routes/publish/__init__.py index 90af3a80..18515f38 100644 --- a/routes/publish/__init__.py +++ b/routes/publish/__init__.py @@ -1 +1 @@ -from . import publish_game, export_scorm +from . import export_scorm diff --git a/routes/publish/publish_game.py b/routes/publish/publish_game.py deleted file mode 100644 index 7cd043af..00000000 --- a/routes/publish/publish_game.py +++ /dev/null @@ -1,28 +0,0 @@ -import os - -from flask import after_this_request, send_file - -import db -from publish.build_package import build_game_zip - -from core.flask_app import app - - -@app.route("/game//publish", methods=["POST"]) -def publish_game(slug): - """Construit et renvoie le zip exécutable autonome du jeu (voir - publish/build_package.py) — protégée par la même garde d'accès que - toutes les autres routes /game//... (core/auth_guard.py), - aucune vérification supplémentaire nécessaire ici.""" - zip_path, game_name = build_game_zip(slug) - - @after_this_request - def _cleanup(response): - try: - os.remove(zip_path) - except OSError: - pass - return response - - download_name = f"{db.slugify(game_name)}.zip" - return send_file(zip_path, as_attachment=True, download_name=download_name, mimetype="application/zip") diff --git a/screens/labels/animal_sprite_library.py b/screens/labels/animal_sprite_library.py index b90bdf81..b5d2afb8 100644 --- a/screens/labels/animal_sprite_library.py +++ b/screens/labels/animal_sprite_library.py @@ -24,7 +24,7 @@ def _load_manifest(): except FileNotFoundError: # Poste de dev sans les assets animaux copiés (script pas encore # lancé), ou paquet exporté pour un jeu qui n'utilise aucun - # personnage animal (voir publish/build_package.py, copie + # personnage animal (voir publish/build_scorm_package.py, copie # sélective) : aucun personnage animal disponible, pas une erreur. return {} diff --git a/screens/payload/full_game_payload.py b/screens/payload/full_game_payload.py index 503f8ae6..376af29b 100644 --- a/screens/payload/full_game_payload.py +++ b/screens/payload/full_game_payload.py @@ -14,20 +14,21 @@ from ..scenes.render_scene_object import render_scene_object def full_game_payload(slug, player_id=db.PLAYER_SHARED): """Toutes les données nécessaires au runtime JS de la page de jeu jouable - (/game//play, ou /jouer/ — voir routes/public_play/) : chaque - écran avec ses éléments (HTML déjà généré, y compris les Répéteurs de - données, lus en direct), le graphe de logique (nœuds + fils) de chaque - écran, et un instantané des données du jeu (avec le type de chaque - champ, nécessaire pour évaluer une condition correctement) — un seul - aller-retour serveur, ensuite tout se joue côté client (aucune - navigation ne recharge la page, sauf pour appliquer une action "modifier - une donnée", qui doit passer par le serveur). + (/game//play, ou le paquet Web/SCORM exporté — voir + publish/build_scorm_package.py) : chaque écran avec ses éléments (HTML + déjà généré, y compris les Répéteurs de données, lus en direct), le + graphe de logique (nœuds + fils) de chaque écran, et un instantané des + données du jeu (avec le type de chaque champ, nécessaire pour évaluer + une condition correctement) — un seul aller-retour serveur, ensuite + tout se joue côté client (aucune navigation ne recharge la page, sauf + pour appliquer une action "modifier une donnée", qui doit passer par + le serveur). - player_id (état par joueur, Phase 1) : PLAYER_SHARED pour l'aperçu - créateur (comportement inchangé — une seule partie partagée pendant la - conception), l'identifiant réel du visiteur pour une partie publique - (voir core/player_identity.py) — chaque objet/variable per_player - renvoie alors la valeur propre à CE joueur.""" + player_id (état par joueur, Phase 1) : toujours PLAYER_SHARED + aujourd'hui (une seule partie partagée, pour la conception comme pour + un paquet exporté) — le paramètre reste générique pour tout objet/ + variable per_player (voir db/global_vars/, db/rows/), qui renvoie + alors la valeur propre à CE joueur.""" # Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un # écran "jeu_2d" construit "elements" depuis _scene_objects (objets de # scène en pixels) plutôt que _screen_elements (DOM en %) — décidé diff --git a/screens/rendering/list_used_forge_characters.py b/screens/rendering/list_used_forge_characters.py index 88aed5e9..0b4394ab 100644 --- a/screens/rendering/list_used_forge_characters.py +++ b/screens/rendering/list_used_forge_characters.py @@ -9,7 +9,7 @@ def list_used_forge_characters(slug): réellement référencés par CE jeu — tous écrans, écrans-modèles compris (même patron que full_game_payload.py : un personnage peut vivre dans un écran-modèle d'élément réutilisable). Utilisé par - publish/build_package.py pour ne copier dans le .zip exporté que les + publish/build_scorm_package.py pour ne copier dans le .zip exporté que les dossiers static/characters/animals/// réellement utilisés par CE jeu, plutôt que les ~52 000 fichiers de toute la bibliothèque (voir le plan "Bibliothèque de sprites animaux CraftPix").""" diff --git a/templates/base.html b/templates/base.html index 53b74526..3392f0ef 100644 --- a/templates/base.html +++ b/templates/base.html @@ -54,26 +54,14 @@ (quiz/embranchement/rpg), la version complète pour "custom". --> 📊 Tableau de bord ▶️ Jouer - - + - -
- - -
- {% if game.is_public_played %} - 🔗 Lien public - {% endif %} {% endif %} @@ -107,89 +95,12 @@ - + dans #pageChrome (comme le bouton "🎓 Exporter (Web/SCORM)" qui + l'ouvre), son script ne serait rejoué qu'au tout premier chargement + complet de la page. --> -