Files
Forge-Engine/screens/payload/full_game_payload.py
T
williamandClaude Sonnet 5 d664ed5637
Build and deploy / test-python (push) Successful in 8m45s
Build and deploy / test-js (push) Successful in 49s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
Supprime le concept de "quête" au profit d'un onglet unique "Déclencheurs"
portant toute la logique (dialogue, condition, marquage terminé) directement
sur l'objet de scène. Ajoute une nouvelle action "Modifier une variable"
(réutilisant le vocabulaire du graphe de flow) utilisable après une
collision, une interaction ou une branche de condition, ainsi qu'un
chaînage d'actions ("then") permettant d'enchaîner plusieurs actions à la
suite et d'étendre un déclencheur déjà posé sans le recréer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 22:05:06 +02:00

175 lines
9.6 KiB
Python

import db
from ..screens_repo.list_screens import list_screens
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 ..rendering.personnage_data import resolve_personnage_animations
from ..rendering.personnage_commands import resolve_personnage_commands
from ..rendering.personnage_role import resolve_personnage_role
from ..rendering.collision_settings import resolve_collision_settings
from ..rendering.collision_rules import resolve_collision_rules
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
from ..rendering.trigger_graph import list_completable_dialogue_ids
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 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) : 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é
# ÉCRAN PAR ÉCRAN (s["kind"]), un même jeu peut mélanger les deux ;
# tout le reste de cette fonction (flows/animations/variables/data)
# est déjà générique et inchangé pour les deux types d'écran.
screens_ = list_screens(slug)
payload_screens = []
flows = {}
animations = {}
for s in screens_:
objects = list_scene_objects(slug, s["id"])
for o in objects:
o["rendered_html"] = render_scene_object(o)
# Déplacement/animation automatiques (voir
# static/js/play/personnage-controller.js) : chaque
# personnage porte ses touches et son rôle déjà résolus
# (défauts comblés) pour que le runtime client n'ait
# aucune logique de repli à dupliquer — seul un
# personnage "joueur" est déplacé/suivi par la caméra.
if o["kind"] == "personnage":
o["personnage_commandes"] = resolve_personnage_commands(o)
o["personnage_role"] = resolve_personnage_role(o)
if o["kind"] in ("dialogue_box", "quiz_box", "score_widget"):
# Widgets d'interface (voir "🖥️ Interface",
# screens/rendering/dialogue_box_style.py) : ni
# collision, ni cible/source de règle — juste leur
# style, lu par static/js/play/dialogue-box-controller.js.
o["dialogue_box_style"] = resolve_dialogue_box_style(o)
continue
# Boîte de collision (voir static/js/play/conditions.js::
# elementsOverlap) : posée sur TOUT objet de scène, pas
# seulement "personnage" — un décor/fond peut aussi servir
# d'obstacle.
o["collision"] = resolve_collision_settings(o)
# Règles "à la collision/dans un périmètre -> action" (voir
# "🧩 Collision", templates/scene_edit.html, et
# static/js/play/collision-rules-controller.js pour leur
# exécution) — jamais sur "fond" (jamais une cible), ni sur
# le personnage "joueur" lui-même (ce sont TOUJOURS les
# AUTRES objets qui réagissent à SA présence, jamais
# l'inverse).
if o["kind"] != "fond" and not (o["kind"] == "personnage" and o.get("personnage_role") == "joueur"):
o["collision_rules"] = resolve_collision_rules(o)
payload_screens.append({**s, "elements": objects})
# Le graphe de logique et les animations sont, eux, lus pour TOUS les
# écrans. 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).
personnage_animations = {}
for s in screens_:
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"])
for o in list_scene_objects(slug, s["id"]):
if o["kind"] == "personnage":
personnage_animations[str(o["id"])] = resolve_personnage_animations(o)
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)
# Un champ "relation" vit dans une colonne SQL "<champ>_id", jamais
# sous son nom "propre" (voir _field_column dans
# filter_repeater_rows.py, la même règle) — sans ce cas particulier,
# un champ relation lisait toujours une colonne inexistante et
# renvoyait systématiquement None ici. Inoffensif EN LIGNE (cette
# snapshot n'est jamais utilisée pour un filtre/une Donnée liée : ces
# derniers requêtent la base fraîche via filter_repeater_rows.py) —
# mais fatal pour le port hors ligne (export Web/SCORM), qui n'a
# QUE cette snapshot, aucune base à requêter (bug signalé par un
# utilisateur : un filtre sur un champ relation ne matchait jamais
# rien une fois exporté, laissant "{{champ}}" affiché tel quel).
cols = {}
for f in full["fields"]:
col = db.slugify(f["name"]).replace("-", "_")
if f["type"] == "relation":
col += "_id"
cols[f["name"]] = col
data[str(d["id"])] = [
{**{fname: r.get(col) for fname, col in cols.items()}, "id": r["id"]}
for r in rows
]
# min_value/max_value (2.2 — bornage automatique, voir
# apply_data_action.py) : nécessaires ici pour que le port
# client-side de l'export Web/SCORM (static/js/play/offline/,
# voir le plan "port complet du runtime jouable côté navigateur")
# puisse reproduire le même bornage sans aller-retour serveur —
# inoffensif pour le runtime en ligne, qui ignore ces deux clés.
fields_meta[str(d["id"])] = [
{"name": f["name"], "type": f["type"], "min_value": f.get("min_value"), "max_value": f.get("max_value")}
for f in full["fields"]
]
# 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)
}
# id de définition -> nom de l'objet (voir filter_repeater_rows.py côté
# serveur, static/js/play/offline/filter-repeater-rows.js côté port
# hors ligne) : nécessaire pour résoudre une référence "{{Objet.champ}}"
# (Répéteur/Donnée liée/Condition de visibilité) — data/fields_meta
# ci-dessus sont déjà indexés par id, jamais par nom.
definition_names = {str(d["id"]): d["name"] for d in definitions}
# Déclencheurs "terminés" (voir screens/rendering/collision_rules.py::
# "dialogue".mark_completed) : GAME-WIDE, la liste des id de dialogue
# que le créateur a marqués comme concluant la partie — nécessaire à
# static/js/play/dialogue-box-controller.js::
# forgeSyncAllDialoguesCompletionToScorm pour savoir quand TOUS ont
# été joués, sans plus aucune notion de quête.
completable_dialogue_ids = list_completable_dialogue_ids(slug)
return {
"screens": payload_screens, "flows": flows, "animations": animations,
"data": data, "fields_meta": fields_meta,
"variables": variables, "personnage_animations": personnage_animations,
"definition_names": definition_names,
"completable_dialogue_ids": completable_dialogue_ids,
}