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 ]