- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance, video, son, visibilite, indication, attendre comme actions, utilisables aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par Ruby (IA), avec blocs deplacables/supprimables dans une chaine. - Corrige plusieurs variantes du bug "impossible de poser un objet hors du champ de la camera" (troncature du chainage d'actions a 4 maillons, fond importe pose a 128x128 au lieu de sa taille reelle, decalage du fond au vrai glisser-depose, redimensionnement manuel jamais propage au monde). - Ajoute un vrai glisser-depose depuis la galerie vers la scene, la gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/ Videos, suppression, reclassement fond<->decor sans re-upload). - Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement, limite 5 min), avec validation de la duree reelle du fichier, et une replique audio optionnelle dans une bulle de dialogue. - Fixe la taille de pose d'un objet/decor importe a 200x200 avec une boite de collision de 150x150. - Filtre le selecteur de fichier des actions son/video par type reel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
226 lines
9.9 KiB
Python
226 lines
9.9 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
|
|
|
|
|
|
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
|
|
]
|