Files
Forge-Engine/screens/payload/full_game_payload.py
T
williamandClaude Sonnet 5 f0070faced
Build and deploy / test-python (push) Successful in 1m56s
Build and deploy / test-js (push) Successful in 8s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase A (2/2) : éditeur de scène 2D — route, template, runtime jouable
Deuxième et dernier commit de la Phase A du plan "Fondations d'une
plateforme multi-éditeurs" (le premier, efa7d1d, posait le schéma et le
CRUD des objets de scène). Ce commit branche l'éditeur et le mode
jouable sur ces fondations, en réutilisant TEL QUEL tout ce qui est déjà
générique côté moteur de logique.

Éditeur (routes/scenes/, templates/scene_edit.html,
static/js/scenes/scene-editor.js) :
- routes/screens/screen_edit.py délègue à render_scene_edit() dès que
  game_type == "jeu_2d" — même endpoint Flask "screen_edit" pour les
  deux éditeurs, aucune route dupliquée.
- Nouveau template scene_edit.html : canevas à taille FIXE en pixels
  (scene_width/scene_height) avec glisser-déposer/redimensionnement en
  pixels (scene-editor.js, mirror pixel de tree-panels.js), galerie
  personnages/décors. Les onglets "Blocs de logique"/"Timeline
  d'animation"/"Événements" réutilisent tels quels flow-editor.js,
  tabs-and-blocks.js et animation-timeline.js.
- flow-editor.js : nouveau flag FLOW_TARGETS_OBJECTS (false par défaut,
  true dans scene_edit.html) qui fait écrire submitNodeForm() vers
  trigger_object_id/target_object_id au lieu de trigger_element_id/
  target_element_id, plus une branche "Modifier un objet de scène"
  (position px, visibilité, orientation) et des gardes null partout
  (le DOM de scene_edit.html n'a pas tous les champs de l'éditeur
  document).
- blocks_view.py étend l'ensemble d'ids d'éléments concernés par un
  bloc pour inclure aussi trigger_object_id/target_object_id.

Runtime jouable (templates/play.html, static/js/play/actions.js,
screens/payload/full_game_payload.py) :
- full_game_payload() construit les écrans d'un jeu jeu_2d à partir de
  _scene_objects (render_scene_object) au lieu de _screen_elements, et
  récupère les animations de personnage sur les objets de scène.
- play.html : #playFrame occupe tout le viewport pour une scène jeu_2d
  (pas de ratio fluide) ; la scène se centre via .playScreen.playScene,
  qui réutilise tel quel showScreen() (déjà indexé par id d'écran, pas
  par forme DOM) — aucun fichier "scenes.js" séparé n'a été nécessaire.
- actions.js : jouer_animation_sprite retombe sur target_object_id si
  target_element_id est absent ; nouvelle action modifier_objet_scene
  avec applyObjectProperty() (positions en px, contrairement aux % de
  applyElementProperty()).

Un bug de gabarit a été découvert et corrigé pendant la vérification :
le commentaire CSS de play.html contenait littéralement "{% for %}"
comme texte français, que Jinja interprétait comme une vraie balise et
faisait planter le rendu — reformulé. render_scene_object() n'était en
outre jamais appelé par la vue de l'éditeur (les objets de scène
s'affichaient sans image) — scene_edit_view.py attache maintenant
rendered_html à chaque objet avant de les passer au template.

Nouveaux tests (tests/test_scene_edit_view.py, 10 cas) : dispatch
document vs jeu_2d, rendu d'un personnage sélectionné, CRUD géométrie/
suppression d'objet, nœuds de flow ciblant un objet de scène (action et
condition de collision), payload et page /play pour un jeu jeu_2d,
présence des nouveaux helpers JS. Suite complète : 309 tests passent
(299 existants + 10 nouveaux, aucune régression). node --check et
node --test (13 tests JS) passent également.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 10:05:40 +02:00

130 lines
7.0 KiB
Python

import db
from ..screens_repo.list_screens import list_screens
from ..elements.list_elements import list_elements
from ..flow.list_flow_nodes import list_flow_nodes
from ..flow.list_flow_edges import list_flow_edges
from ..animations.list_animation_clips import list_animation_clips
from ..element_types.list_element_types import list_element_types
from ..rendering.personnage_data import resolve_personnage_animations
from ..scenes.list_scene_objects import list_scene_objects
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/<slug>/play, ou /jouer/<slug> — 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).
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."""
# Fondations multi-éditeurs — un jeu "jeu_2d" (voir db/games/
# get_game_type.py) construit "elements" depuis _scene_objects
# (objets de scène en pixels) plutôt que _screen_elements (DOM en %) ;
# tout le reste de cette fonction (flows/animations/variables/data)
# est déjà générique et inchangé pour les deux types de jeu.
is_scene_game = db.get_game_type(slug) == "jeu_2d"
screens_ = list_screens(slug)
payload_screens = []
flows = {}
animations = {}
for s in screens_:
if is_scene_game:
objects = list_scene_objects(slug, s["id"])
for o in objects:
o["rendered_html"] = render_scene_object(o)
payload_screens.append({**s, "elements": objects})
else:
elements = list_elements(slug, s["id"], enforce_visibility=True, player_id=player_id)
payload_screens.append({**s, "elements": elements})
# Le graphe de logique et les animations sont, eux, lus pour TOUS les
# écrans, écrans-modèles compris (include_templates=True) — pas
# seulement les "vrais" écrans ci-dessus. Un élément de jeu réutilisable
# (ex. "mail content") peut avoir son propre déclencheur/sa propre
# animation posée dans l'éditeur de SON écran-modèle (voir
# screen_edit.html) : sans ses entrées ici, gameData.flows/
# gameData.animations n'auraient jamais contenu cet écran-modèle côté
# client, et findTriggerNode()/collectAnimationClips() (templates/
# play.html) — pourtant déjà écrits pour les chercher — n'auraient
# jamais rien trouvé. On ne les ajoute PAS à payload_screens : un
# écran-modèle ne doit jamais être rendu comme un vrai <div
# class="playScreen"> (il n'est jamais affiché tel quel, seulement
# rechargé en direct à l'intérieur d'un élément qui l'utilise).
# Animations d'un personnage (Phase 8) — résolues ici, à CHAQUE
# affichage de la page de jeu (donc toujours à partir du personnage
# Forge ACTUELLEMENT assigné), plutôt que figées dans le nœud de flow/
# le clip de Timeline au moment où le créateur les a configurés : sans
# ça, changer le personnage Forge d'un élément dans l'éditeur laissait
# ses animations déjà posées dans la logique/la Timeline continuer à
# jouer les frames de l'ANCIEN personnage indéfiniment (bug signalé —
# voir runActionNode()/applyAnimationClip() qui font cette résolution
# à l'exécution à partir de gameData.personnage_animations plutôt que
# depuis un data_value/custom_keyframes qui contiendrait des frames
# déjà résolues). include_templates=True : un personnage peut vivre
# dans un écran-modèle d'élément de jeu réutilisable, comme les flows/
# animations ci-dessous.
personnage_animations = {}
for s in list_screens(slug, include_templates=True):
flows[str(s["id"])] = {
"nodes": list_flow_nodes(slug, s["id"]),
"edges": list_flow_edges(slug, s["id"]),
}
animations[str(s["id"])] = list_animation_clips(slug, s["id"])
if is_scene_game:
for o in list_scene_objects(slug, s["id"]):
if o["kind"] == "personnage":
personnage_animations[str(o["id"])] = resolve_personnage_animations(o)
else:
for el in list_elements(slug, s["id"]):
if el.get("widget") == "personnage":
personnage_animations[str(el["id"])] = resolve_personnage_animations(el)
definitions = db.list_definitions(slug)
data = {}
fields_meta = {}
for d in definitions:
full = db.get_definition(slug, d["id"])
rows = db.list_rows(slug, full, player_id)
cols = {f["name"]: db.slugify(f["name"]).replace("-", "_") for f in full["fields"]}
data[str(d["id"])] = [
{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]}
for r in rows
]
fields_meta[str(d["id"])] = [{"name": f["name"], "type": f["type"]} for f in full["fields"]]
# element_type_id -> id de son écran-modèle : nécessaire côté client (voir
# collectAnimationClips() dans play.html) pour savoir, quand un élément de
# jeu réutilisable (ex. "mail content") est posé sur un écran, quels
# autres clips d'animation (ceux de SON PROPRE écran-modèle) doivent
# aussi être joués à l'affichage de cet écran — sans quoi une animation
# posée directement dans l'éditeur du modèle ne se jouait jamais quand le
# modèle est utilisé ailleurs.
element_types = {str(t["id"]): t["template_screen_id"] for t in list_element_types(slug)}
# Instantané des variables globales, pour qu'un nœud Condition puisse en
# tester une côté CLIENT (evaluateConditionClause, templates/play.html) —
# {{$var}} (Répéteur/Condition de visibilité), lui, reste résolu côté
# SERVEUR au rendu (filter_repeater_rows.py) et n'a jamais eu besoin de
# ça. refreshRuntimeData() récupère un payload entier (donc des
# variables à jour) après toute action qui en modifie une.
variables = {
v["name"]: {"value": v["value"], "type": v["type"]}
for v in db.list_global_variables_for_player(slug, player_id)
}
return {
"screens": payload_screens, "flows": flows, "animations": animations,
"element_types": element_types, "data": data, "fields_meta": fields_meta,
"variables": variables, "personnage_animations": personnage_animations,
}