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>
130 lines
7.0 KiB
Python
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,
|
|
}
|