Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
- 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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b07b231a61
commit
559331f9cf
@@ -257,10 +257,14 @@ def test_sanitize_collision_rules_keeps_a_then_chain_of_leaves():
|
||||
def test_sanitize_collision_rules_bounds_a_then_chain_that_is_too_long():
|
||||
"""_MAX_THEN_CHAIN borne la chaîne — une chaîne trop longue postée à la
|
||||
main est coupée plutôt que rejetée entièrement (même tolérance que le
|
||||
reste de ce moteur)."""
|
||||
reste de ce moteur). Bug corrigé : à 4 (l'ancienne valeur), un
|
||||
créateur enchaînant "attendre -> dialogue -> surbrillance -> attendre"
|
||||
(4 maillons, un scénario tout à fait normal) voyait déjà TOUT
|
||||
5e maillon ajouté ensuite tronqué en silence — voir
|
||||
screens/rendering/collision_rules.py::_MAX_THEN_CHAIN."""
|
||||
chain = {"type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"}
|
||||
node = chain
|
||||
for _ in range(10): # largement au-delà de _MAX_THEN_CHAIN (4)
|
||||
for _ in range(50): # largement au-delà de _MAX_THEN_CHAIN (30)
|
||||
node["then"] = {"type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"}
|
||||
node = node["then"]
|
||||
rules = screens.sanitize_collision_rules([{"trigger": "collision", "action": chain}])
|
||||
@@ -269,7 +273,36 @@ def test_sanitize_collision_rules_bounds_a_then_chain_that_is_too_long():
|
||||
while node:
|
||||
depth += 1
|
||||
node = node.get("then")
|
||||
assert depth == 4 # _MAX_THEN_CHAIN, jamais plus
|
||||
assert depth == 30 # _MAX_THEN_CHAIN, jamais plus
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_a_then_chain_of_five_leaves_the_bug_that_broke_the_user():
|
||||
"""Reproduction directe du bug signalé : attendre -> dialogue ->
|
||||
surbrillance -> attendre -> surbrillance (5 maillons) doit survivre
|
||||
intact à la resanitisation qui suit chaque écriture (voir
|
||||
append_action_to_screen_trigger/set_then_action_in_rules)."""
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "attendre", "id": "a_1", "data_value": "2",
|
||||
"then": {
|
||||
"type": "dialogue", "dialogue": {"id": "d_1", "lines": []},
|
||||
"then": {
|
||||
"type": "surbrillance", "id": "s_1", "object_id": 37, "valeur": "on",
|
||||
"then": {
|
||||
"type": "attendre", "id": "a_2", "data_value": "3",
|
||||
"then": {"type": "surbrillance", "id": "s_2", "object_id": 37, "valeur": "off"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}])
|
||||
depth = 0
|
||||
node = rules[0]["action"]
|
||||
while node:
|
||||
depth += 1
|
||||
node = node.get("then")
|
||||
assert depth == 5
|
||||
|
||||
|
||||
def test_set_then_action_in_rules_attaches_a_new_leaf_at_the_end_of_a_dialogue_chain():
|
||||
@@ -300,7 +333,11 @@ def test_set_then_action_in_rules_returns_false_for_an_unknown_leaf_id():
|
||||
assert screens.set_then_action_in_rules(rules, "d_absent", {"type": "variable", "target_variable": "score", "data_operation": "incrementer"}) is False
|
||||
|
||||
|
||||
def test_set_then_action_in_rules_never_overwrites_an_existing_then():
|
||||
def test_set_then_action_in_rules_inserts_between_a_leaf_and_its_existing_then():
|
||||
"""Demande explicite : "pouvoir ajouter n'importe quelle action à
|
||||
n'importe quel niveau du schéma" — la suite déjà posée (ici "score")
|
||||
passe DERRIÈRE la nouvelle action insérée ("vies"), jamais écrasée
|
||||
(comportement précédent : refuser purement et simplement)."""
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
@@ -309,8 +346,321 @@ def test_set_then_action_in_rules_never_overwrites_an_existing_then():
|
||||
},
|
||||
}])
|
||||
attached = screens.set_then_action_in_rules(rules, "d_1", {"type": "variable", "target_variable": "vies", "data_operation": "decrementer", "data_value": "1"})
|
||||
assert attached is False
|
||||
assert rules[0]["action"]["then"]["target_variable"] == "score" # inchangé
|
||||
assert attached is True
|
||||
action = rules[0]["action"]
|
||||
assert action["then"]["target_variable"] == "vies"
|
||||
assert action["then"]["then"] == {"type": "variable", "id": "v_1", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"}
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_accepts_clic_and_survol_triggers():
|
||||
""""clic"/"survol" — nouveaux déclencheurs sans besoin d'un joueur,
|
||||
voir screens/rendering/collision_rules.py."""
|
||||
rules = screens.sanitize_collision_rules([
|
||||
{"trigger": "clic", "action": {"type": "dialogue"}},
|
||||
{"trigger": "survol", "action": {"type": "dialogue"}},
|
||||
])
|
||||
assert [r["trigger"] for r in rules] == ["clic", "survol"]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_a_surbrillance_action():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "clic",
|
||||
"action": {"type": "surbrillance", "object_id": 7, "valeur": "toggle"},
|
||||
}])
|
||||
assert rules == [{"trigger": "clic", "action": {"type": "surbrillance", "object_id": 7, "valeur": "toggle"}}]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_a_surbrillance_action_with_an_unknown_valeur_or_object_id():
|
||||
rules = screens.sanitize_collision_rules([
|
||||
{"trigger": "clic", "action": {"type": "surbrillance", "object_id": 7, "valeur": "sorcellerie"}},
|
||||
{"trigger": "clic", "action": {"type": "surbrillance", "object_id": "pas-un-id", "valeur": "on"}},
|
||||
])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_a_visibilite_action():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "visibilite", "object_id": 3, "valeur": "masque"},
|
||||
}])
|
||||
assert rules == [{"trigger": "collision", "action": {"type": "visibilite", "object_id": 3, "valeur": "masque"}}]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_a_visibilite_action_with_an_unknown_valeur():
|
||||
rules = screens.sanitize_collision_rules([
|
||||
{"trigger": "collision", "action": {"type": "visibilite", "object_id": 3, "valeur": "sorcellerie"}},
|
||||
])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_a_son_action():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "clic",
|
||||
"action": {"type": "son", "asset_url": "/assets/1/bip.mp3"},
|
||||
}])
|
||||
assert rules == [{"trigger": "clic", "action": {"type": "son", "asset_url": "/assets/1/bip.mp3"}}]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_a_son_action_without_asset_url():
|
||||
rules = screens.sanitize_collision_rules([{"trigger": "clic", "action": {"type": "son", "asset_url": ""}}])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_a_video_action():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "survol",
|
||||
"action": {"type": "video", "asset_url": "/assets/1/intro.mp4", "mode": "plein_ecran"},
|
||||
}])
|
||||
assert rules == [{
|
||||
"trigger": "survol",
|
||||
"action": {"type": "video", "asset_url": "/assets/1/intro.mp4", "mode": "plein_ecran"},
|
||||
}]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_a_video_action_with_an_unknown_mode():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "survol",
|
||||
"action": {"type": "video", "asset_url": "/assets/1/intro.mp4", "mode": "sorcellerie"},
|
||||
}])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_an_indication_action_with_and_without_duree_ms():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "indication", "object_id": 5, "texte": "Regarde ici !", "duree_ms": 2000},
|
||||
}])
|
||||
assert rules == [{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "indication", "object_id": 5, "texte": "Regarde ici !", "duree_ms": 2000},
|
||||
}]
|
||||
rules_no_duree = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "indication", "object_id": 5, "texte": "Regarde ici !"},
|
||||
}])
|
||||
assert "duree_ms" not in rules_no_duree[0]["action"]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_an_indication_action_without_texte():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "indication", "object_id": 5, "texte": " "},
|
||||
}])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_chains_a_video_then_a_dialogue():
|
||||
"""Une "video" reste une FEUILLE chaînable, comme dialogue/variable
|
||||
(demande explicite : pouvoir enchaîner plusieurs actions)."""
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "video", "id": "v_1", "asset_url": "/assets/1/intro.mp4", "mode": "plein_ecran",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
}])
|
||||
assert rules[0]["action"]["then"] == {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_keeps_an_attendre_action():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "attendre", "data_value": "2.5"},
|
||||
}])
|
||||
assert rules == [{"trigger": "collision", "action": {"type": "attendre", "data_value": "2.5"}}]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_an_attendre_action_with_an_invalid_or_negative_value():
|
||||
rules = screens.sanitize_collision_rules([
|
||||
{"trigger": "collision", "action": {"type": "attendre", "data_value": "pas-un-nombre"}},
|
||||
{"trigger": "collision", "action": {"type": "attendre", "data_value": "-1"}},
|
||||
])
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_chains_an_attendre_then_a_dialogue():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "attendre", "id": "a_1", "data_value": "2",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
}])
|
||||
assert rules[0]["action"]["then"] == {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}
|
||||
|
||||
|
||||
def test_set_then_action_in_rules_addresses_a_surbrillance_leaf_by_its_own_id():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "clic",
|
||||
"action": {"type": "surbrillance", "id": "s_1", "object_id": 7, "valeur": "on"},
|
||||
}])
|
||||
attached = screens.set_then_action_in_rules(rules, "s_1", {"type": "son", "asset_url": "/assets/1/bip.mp3"})
|
||||
assert attached is True
|
||||
assert rules[0]["action"]["then"] == {"type": "son", "asset_url": "/assets/1/bip.mp3"}
|
||||
|
||||
|
||||
def test_remove_action_from_rules_removes_the_whole_rule_when_removing_a_single_head_leaf():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
}])
|
||||
removed = screens.remove_action_from_rules(rules, "d_1")
|
||||
assert removed is True
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_remove_action_from_rules_promotes_the_then_when_removing_the_head_of_a_chain():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "attendre", "id": "a_1", "data_value": "2",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
}])
|
||||
removed = screens.remove_action_from_rules(rules, "a_1")
|
||||
assert removed is True
|
||||
assert rules == [{"trigger": "collision", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}}]
|
||||
|
||||
|
||||
def test_remove_action_from_rules_removes_a_middle_leaf_and_reconnects_the_chain():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "variable", "id": "v_1", "target_variable": "score", "data_operation": "incrementer", "data_value": "1",
|
||||
"then": {
|
||||
"type": "attendre", "id": "a_1", "data_value": "2",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
},
|
||||
}])
|
||||
removed = screens.remove_action_from_rules(rules, "a_1")
|
||||
assert removed is True
|
||||
action = rules[0]["action"]
|
||||
assert action["type"] == "variable"
|
||||
assert action["then"] == {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}
|
||||
|
||||
|
||||
def test_remove_action_from_rules_returns_false_for_an_unknown_leaf_id():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
}])
|
||||
assert screens.remove_action_from_rules(rules, "d_absent") is False
|
||||
assert len(rules) == 1
|
||||
|
||||
|
||||
def test_remove_action_from_rules_removes_a_leaf_inside_a_condition_branch():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "condition", "variable": "score", "operateur": "egal", "valeur": "1",
|
||||
"si_vrai": {"type": "dialogue", "dialogue": {"id": "d_vrai", "lines": []}}, "si_faux": None,
|
||||
},
|
||||
}])
|
||||
removed = screens.remove_action_from_rules(rules, "d_vrai")
|
||||
assert removed is True
|
||||
assert rules[0]["action"]["si_vrai"] is None
|
||||
|
||||
|
||||
def test_remove_action_from_rules_removes_interagir_entirely_when_its_only_sub_action_is_removed():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "interagir", "sub_action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}},
|
||||
}])
|
||||
removed = screens.remove_action_from_rules(rules, "d_1")
|
||||
assert removed is True
|
||||
assert rules == []
|
||||
|
||||
|
||||
def test_move_action_in_rules_swaps_adjacent_leaves_up_and_down():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "variable", "id": "v_1", "target_variable": "score", "data_operation": "incrementer", "data_value": "1",
|
||||
"then": {
|
||||
"type": "attendre", "id": "a_1", "data_value": "2",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
},
|
||||
}])
|
||||
moved = screens.move_action_in_rules(rules, "a_1", "up")
|
||||
assert moved is True
|
||||
action = rules[0]["action"]
|
||||
assert action["type"] == "attendre"
|
||||
assert action["then"]["type"] == "variable"
|
||||
assert action["then"]["then"] == {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}
|
||||
|
||||
|
||||
def test_move_action_in_rules_at_the_start_of_the_chain_moving_up_has_no_effect_but_still_found():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "variable", "id": "v_1", "target_variable": "score", "data_operation": "incrementer", "data_value": "1",
|
||||
"then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
},
|
||||
}])
|
||||
moved = screens.move_action_in_rules(rules, "v_1", "up")
|
||||
assert moved is True
|
||||
assert rules[0]["action"]["target_variable"] == "score" # inchangé, déjà en tête
|
||||
|
||||
|
||||
def test_move_action_in_rules_returns_false_for_an_unknown_leaf_id():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
}])
|
||||
assert screens.move_action_in_rules(rules, "d_absent", "up") is False
|
||||
|
||||
|
||||
def test_backfill_missing_leaf_ids_assigns_a_stable_id_to_a_variable_leaf_without_one():
|
||||
"""Bug corrigé : une action "variable" posée sans "id" (ex. par l'IA,
|
||||
ce champ restant optionnel dans son schéma, ou une session
|
||||
antérieure à cette convention) restait à jamais impossible à
|
||||
adresser — "+ Ajouter une action" n'apparaissait jamais après elle."""
|
||||
rules = [{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"},
|
||||
}]
|
||||
changed = screens.backfill_missing_leaf_ids(rules)
|
||||
assert changed is True
|
||||
assert rules[0]["action"]["id"]
|
||||
|
||||
|
||||
def test_backfill_missing_leaf_ids_reaches_inside_condition_branches_and_then_chains():
|
||||
rules = [{
|
||||
"trigger": "collision",
|
||||
"action": {
|
||||
"type": "condition", "variable": "x", "operateur": "egal", "valeur": "1",
|
||||
"si_vrai": {
|
||||
"type": "dialogue", "dialogue": {"id": "d_1", "lines": []},
|
||||
"then": {"type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"},
|
||||
},
|
||||
"si_faux": None,
|
||||
},
|
||||
}]
|
||||
changed = screens.backfill_missing_leaf_ids(rules)
|
||||
assert changed is True
|
||||
assert rules[0]["action"]["si_vrai"]["then"]["id"]
|
||||
|
||||
|
||||
def test_backfill_missing_leaf_ids_is_a_noop_when_every_leaf_already_has_one():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "variable", "id": "v_1", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"},
|
||||
}])
|
||||
changed = screens.backfill_missing_leaf_ids(rules)
|
||||
assert changed is False
|
||||
assert rules[0]["action"]["id"] == "v_1"
|
||||
|
||||
|
||||
def test_backfill_missing_leaf_ids_never_touches_a_dialogue_leaf_which_already_has_its_own_id():
|
||||
rules = screens.sanitize_collision_rules([{
|
||||
"trigger": "collision",
|
||||
"action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}},
|
||||
}])
|
||||
changed = screens.backfill_missing_leaf_ids(rules)
|
||||
assert changed is False
|
||||
assert "id" not in rules[0]["action"]
|
||||
|
||||
|
||||
def test_sanitize_collision_rules_drops_malformed_or_unknown_rules():
|
||||
|
||||
Reference in New Issue
Block a user