Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
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

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>
This commit is contained in:
william
2026-09-07 22:05:06 +02:00
co-authored by Claude Sonnet 5
parent c504ace167
commit d664ed5637
62 changed files with 3129 additions and 2266 deletions
+199
View File
@@ -0,0 +1,199 @@
def _game_objects_with_thumbnails(slug, screens_mod):
"""Objets de TOUTES les scènes "jeu_2d" du jeu (contrairement à
screens.list_scene_objects, qui ne prend qu'un screen_id) —
rendered_html ajouté comme l'exigent screens.build_collision_rule_objects
(voir routes/scenes/scene_edit_view.py pour le même patron, une seule
scène)."""
result = []
for s in screens_mod.list_screens(slug):
if s["kind"] != "jeu_2d":
continue
objects = screens_mod.list_scene_objects(slug, s["id"])
for o in objects:
o["rendered_html"] = screens_mod.render_scene_object(o)
o["screen_id"] = s["id"]
o["screen_name"] = s["name"]
result.extend(objects)
return result
def _describe_leaf(action):
"""Une seule feuille ("dialogue" ou "variable", voir
screens/rendering/collision_rules.py) décrite pour l'affichage,
SANS son éventuel "then" (voir _describe_chain, qui aplatit la
chaîne complète)."""
if action.get("type") == "dialogue":
return {"type": "dialogue", "dialogue": action.get("dialogue")}
return {
"type": "variable",
"id": action.get("id"),
"target_variable": action.get("target_variable"),
"data_operation": action.get("data_operation"),
"data_value": action.get("data_value"),
}
def _describe_chain(action):
"""Aplatit une feuille + ses "then" successifs en une LISTE, dans
l'ordre d'exécution — voir maquette validée ("plusieurs actions
chaînées"). Liste vide si `action` n'est ni "dialogue" ni "variable"."""
chain = []
node = action
while isinstance(node, dict) and node.get("type") in ("dialogue", "variable"):
chain.append(_describe_leaf(node))
node = node.get("then")
return chain
def _branch_info(branch):
if not isinstance(branch, dict) or branch.get("type") not in ("dialogue", "variable"):
return None
return {"chain": _describe_chain(branch)}
def _resolve_action_trigger(action, has_interagir=False):
"""Décrit ENTIÈREMENT un arbre d'action (voir screens/rendering/
collision_rules.py) pour l'affichage dans l'onglet "Déclencheurs" —
chaque feuille ("dialogue"/"variable") affiche DIRECTEMENT son propre
contenu, où qu'elle soit dans l'arbre (y compris CHAQUE branche d'une
"condition", indépendamment l'une de l'autre), et sa chaîne "then"
complète (voir _describe_chain). Retourne {has_interagir, chain,
condition} — `chain` est une LISTE (jamais None, vide si l'action ne
se termine par aucune feuille reconnue) ; `condition` est None sauf
pour une action "condition", auquel cas il porte {variable, operateur,
valeur, si_vrai, si_faux} où si_vrai/si_faux valent chacun None ("ne
rien faire") ou {chain: [...]}."""
if not isinstance(action, dict):
return {"has_interagir": has_interagir, "chain": [], "condition": None}
action_type = action.get("type")
if action_type in ("dialogue", "variable"):
return {"has_interagir": has_interagir, "chain": _describe_chain(action), "condition": None}
if action_type == "interagir":
return _resolve_action_trigger(action.get("sub_action") or {}, has_interagir=True)
if action_type == "condition":
return {
"has_interagir": has_interagir, "chain": [],
"condition": {
"variable": action.get("variable"), "operateur": action.get("operateur"),
"valeur": action.get("valeur"),
"si_vrai": _branch_info(action.get("si_vrai")),
"si_faux": _branch_info(action.get("si_faux")),
},
}
return {"has_interagir": has_interagir, "chain": [], "condition": None}
def list_all_triggers(slug):
"""TOUS les déclencheurs du jeu, un par occurrence de "À la collision"
posée sur un objet de scène — remplace l'ancienne recherche inversée
par quête : plus de filtrage par id, chaque déclencheur affiche
directement sa propre logique (voir onglet "Déclencheurs",
static/js/triggers/trigger-editor.js)."""
import screens as screens_mod
objects = _game_objects_with_thumbnails(slug, screens_mod)
rule_objects = screens_mod.build_collision_rule_objects(objects)
result = []
for obj in rule_objects:
for rule in obj["rules"]:
info = _resolve_action_trigger(rule["action"])
result.append({
"object_id": obj["id"],
"object_name": obj["name"],
"thumbnail_html": obj["thumbnail_html"],
"has_interagir": info["has_interagir"],
"trigger": rule["trigger"],
"condition": info["condition"],
"chain": info["chain"],
})
return result
def _collect_dialogue_lines_from_action(action, acc):
if not isinstance(action, dict):
return
action_type = action.get("type")
if action_type == "dialogue":
dialogue = action.get("dialogue")
if isinstance(dialogue, dict):
acc.append(dialogue.get("lines") or [])
_collect_dialogue_lines_from_action(action.get("then"), acc)
elif action_type == "variable":
_collect_dialogue_lines_from_action(action.get("then"), acc)
elif action_type == "interagir":
_collect_dialogue_lines_from_action(action.get("sub_action"), acc)
elif action_type == "condition":
_collect_dialogue_lines_from_action(action.get("si_vrai"), acc)
_collect_dialogue_lines_from_action(action.get("si_faux"), acc)
def collect_all_dialogue_lines(slug):
"""Toutes les répliques/questions de TOUS les dialogues embarqués dans
les règles de collision de TOUS les objets de scène du jeu (voir
screens/rendering/collision_rules.py::"dialogue".dialogue), y compris
au fil d'une chaîne "then" — une liste de listes de lignes (une par
dialogue trouvé), jamais fusionnée ici. Utilisée pour calculer le
score max possible du quiz à l'export SCORM (voir
publish/build_scorm_package.py)."""
import screens as screens_mod
acc = []
for s in screens_mod.list_screens(slug):
if s["kind"] != "jeu_2d":
continue
for obj in screens_mod.list_scene_objects(slug, s["id"]):
for rule in screens_mod.resolve_collision_rules(obj):
_collect_dialogue_lines_from_action(rule.get("action"), acc)
return acc
def _collect_completable_ids_from_action(action, acc):
if not isinstance(action, dict):
return
action_type = action.get("type")
if action_type == "dialogue":
dialogue = action.get("dialogue")
if action.get("mark_completed") and isinstance(dialogue, dict) and dialogue.get("id"):
acc.append(dialogue["id"])
_collect_completable_ids_from_action(action.get("then"), acc)
elif action_type == "variable":
_collect_completable_ids_from_action(action.get("then"), acc)
elif action_type == "interagir":
_collect_completable_ids_from_action(action.get("sub_action"), acc)
elif action_type == "condition":
_collect_completable_ids_from_action(action.get("si_vrai"), acc)
_collect_completable_ids_from_action(action.get("si_faux"), acc)
def list_completable_dialogue_ids(slug):
"""Tous les id de dialogue du jeu dont l'action porte
`mark_completed` (voir screens/rendering/collision_rules.py), y
compris au fil d'une chaîne "then" — utilisée par static/js/play/
dialogue-box-controller.js::forgeSyncAllDialoguesCompletionToScorm
pour savoir quand la partie est terminée (TOUS joués), sans plus
aucune notion de quête."""
import screens as screens_mod
acc = []
for s in screens_mod.list_screens(slug):
if s["kind"] != "jeu_2d":
continue
for obj in screens_mod.list_scene_objects(slug, s["id"]):
for rule in screens_mod.resolve_collision_rules(obj):
_collect_completable_ids_from_action(rule.get("action"), acc)
return acc
def list_all_collision_objects(slug):
"""Catalogue de TOUS les objets éligibles à une règle de collision,
toutes scènes "jeu_2d" confondues — pour peupler le sélecteur d'objet
de la modale d'ajout de déclencheur (voir routes/triggers/)."""
import screens as screens_mod
objects = _game_objects_with_thumbnails(slug, screens_mod)
rule_objects = screens_mod.build_collision_rule_objects(objects)
return [
{"id": o["id"], "name": o["name"], "kind": o["kind"], "thumbnail_html": o["thumbnail_html"]}
for o in rule_objects
]