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>
200 lines
8.5 KiB
Python
200 lines
8.5 KiB
Python
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
|
|
]
|