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 from .collision_rules import LEAF_ACTION_TYPES # Champs affichés pour chaque type de feuille (voir screens/rendering/ # collision_rules.py pour la forme exacte de chacune) — un simple filtre # sur l'action déjà sanitizée, jamais une recopie de valeur : ajouter un # type de feuille suffit à l'ajouter ici, sans dupliquer sa logique de # sanitisation. _LEAF_DISPLAY_FIELDS = { "dialogue": ("dialogue",), "variable": ("id", "target_variable", "data_operation", "data_value"), "surbrillance": ("id", "object_id", "valeur"), "visibilite": ("id", "object_id", "valeur"), "son": ("id", "asset_url"), "video": ("id", "asset_url", "mode"), "indication": ("id", "object_id", "texte", "duree_ms"), "attendre": ("id", "data_value"), } def _describe_leaf(action): """Une seule feuille (voir LEAF_ACTION_TYPES, screens/rendering/ collision_rules.py) décrite pour l'affichage, SANS son éventuel "then" (voir _describe_chain, qui aplatit la chaîne complète).""" action_type = action.get("type") fields = _LEAF_DISPLAY_FIELDS.get(action_type, ()) return {"type": action_type, **{f: action[f] for f in fields if f in action}} 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 pas une feuille reconnue (voir LEAF_ACTION_TYPES).""" chain = [] node = action while isinstance(node, dict) and node.get("type") in LEAF_ACTION_TYPES: 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 LEAF_ACTION_TYPES: 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 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 LEAF_ACTION_TYPES: 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: # Rétro-remplissage des feuilles posées avant que "id" ne devienne # systématique (ex. par l'IA, ce champ restant optionnel dans son # schéma) — sans ça, "+ Ajouter une action"/le retrait/le # déplacement restent silencieusement indisponibles sur ce bloc # précis (voir screens/rendering/collision_rules.py:: # backfill_missing_leaf_ids). Persisté dès ce premier affichage, # jamais régénéré ensuite. if screens_mod.backfill_missing_leaf_ids(obj["rules"]): screens_mod.set_scene_object_collision_rules(slug, obj["id"], obj["rules"]) 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 in LEAF_ACTION_TYPES: _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) for rule in screens_mod.resolve_screen_triggers(s): _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 in LEAF_ACTION_TYPES: _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) for rule in screens_mod.resolve_screen_triggers(s): _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 ]