Files
Forge-Engine/publish/build_package.py
T
williamandClaude Sonnet 5 d2c2f20b65
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 7 : personnage animé par sprites (poses/images successives)
Ajoute une nouvelle action de flow "jouer_animation_sprite" qui joue une
séquence de PNG sur un élément image (cycle en boucle type marche, ou
une fois type saut) — réutilise target_element_id (déjà whitelisté) et
data_value en JSON {frames, fps, loop}, exactement le patron déjà établi
par "jouer_son" (Phase 6) et "alea" (Phase 2) : aucune nouvelle colonne,
aucune migration de schéma.

Ajoute une propriété d'élément "orientation" (Modifier un élément) pour
retourner un personnage en miroir (gauche/droite/bascule) sans nécessiter
une 2e feuille de sprites "vue de dos" — même patron que "surbrillance"/
"désactivé".

Étend aussi la Timeline d'animation existante (kind="sprite", aux côtés
de "animate_css"/"custom") pour qu'un personnage puisse animer tout seul
dès l'affichage de l'écran (ex. idle en boucle perpétuelle), pas
seulement en réaction à un événement — réutilise la colonne générique
custom_keyframes (JSON) et le réglage iteration_count déjà là pour la
boucle, aucune migration non plus. Les deux entrées (action de flow et
clip de timeline) partagent le même moteur côté client
(runSpriteAnimation()/activeSpriteAnimations dans static/js/play/
actions.js, indexé par nœud DOM plutôt que par id d'élément pour
supporter plusieurs instances d'un même écran-modèle animées
indépendamment).

Une bibliothèque de sprites Forge (2 personnages Kenney CC0, sous-
ensemble curé idle/marche/saut copié dans static/characters/) est
proposée dans l'éditeur, mais un créateur peut tout aussi bien utiliser
ses propres sprites uploadés (même route d'upload générique que le son
de la Phase 6).

Périmètre volontairement limité à ce qui a été demandé : pas d'avatar
modulable en couches (assemblage cheveux/haut/bas par le joueur),
écarté du plan initial à la demande de l'utilisateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 19:18:07 +02:00

114 lines
4.7 KiB
Python

"""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
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_engine_sources(staging_dir):
"""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")
for item in _STATIC_ITEMS:
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)
_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"]