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>
175 lines
9.6 KiB
Python
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,
|
|
}
|