"""Déclencheurs D'ÉCRAN (voir game_engine/rendering/screen_triggers.py) : une règle "déclencheur (affichage) -> action" posée sur un ÉCRAN plutôt que sur un objet — SANS objet requis, pour de la narration/cinématique dès l'affichage. Section "Déclencheurs de l'écran" de l'éditeur de scène.""" from collections.abc import Callable from typing import Any from flask.testing import FlaskClient import game_engine def _create_jeu2d_game( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str], name: str = "pytest_screen_triggers" ) -> Any: resp = client.post("/games/new", data={"name": name}, follow_redirects=False) slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1]) screen_id = game_engine.create_screen(slug, "Scène 1", kind="jeu_2d") return slug, screen_id def test_freshly_created_screen_has_no_screen_triggers( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) screen = game_engine.get_screen(slug, screen_id) assert game_engine.resolve_screen_triggers(screen) == [] def test_sanitize_screen_triggers_keeps_an_affichage_trigger() -> None: rules = game_engine.sanitize_screen_triggers( [ {"trigger": "affichage", "action": {"type": "dialogue"}}, ] ) assert rules == [{"trigger": "affichage", "action": {"type": "dialogue"}}] def test_sanitize_screen_triggers_rejects_triggers_not_allowed_at_screen_level() -> None: """Un déclencheur d'OBJET ("collision"/"clic"/"survol") n'a pas de sens sans objet — seul "affichage" est autorisé pour un déclencheur d'écran (voir TRIGGER_TYPES_SCREEN).""" rules = game_engine.sanitize_screen_triggers( [ {"trigger": "collision", "action": {"type": "dialogue"}}, {"trigger": "clic", "action": {"type": "dialogue"}}, {"trigger": "survol", "action": {"type": "dialogue"}}, ] ) assert rules == [] def test_sanitize_screen_triggers_keeps_a_video_action() -> None: rules = game_engine.sanitize_screen_triggers( [ { "trigger": "affichage", "action": {"type": "video", "asset_url": "/assets/1/intro.mp4", "mode": "plein_ecran"}, } ] ) assert rules == [ { "trigger": "affichage", "action": {"type": "video", "asset_url": "/assets/1/intro.mp4", "mode": "plein_ecran"}, } ] def test_sanitize_screen_triggers_keeps_an_attendre_then_dialogue_chain() -> None: """Cas d'usage demandé : "à l'affichage de l'écran" -> attendre N secondes -> déclencher un dialogue.""" rules = game_engine.sanitize_screen_triggers( [ { "trigger": "affichage", "action": { "type": "attendre", "data_value": "3", "then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, }, } ] ) assert rules[0]["action"]["type"] == "attendre" assert rules[0]["action"]["then"]["type"] == "dialogue" def test_set_screen_triggers_persists_and_resolve_reads_it_back( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers(slug, screen_id, [{"trigger": "affichage", "action": {"type": "dialogue"}}]) screen = game_engine.get_screen(slug, screen_id) assert game_engine.resolve_screen_triggers(screen) == [{"trigger": "affichage", "action": {"type": "dialogue"}}] def test_screen_triggers_are_isolated_between_screens( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id_1 = _create_jeu2d_game(client, tmp_game_slug_cleanup) screen_id_2 = game_engine.create_screen(slug, "Scène 2", kind="jeu_2d") game_engine.set_screen_triggers(slug, screen_id_1, [{"trigger": "affichage", "action": {"type": "dialogue"}}]) screen_2 = game_engine.get_screen(slug, screen_id_2) assert game_engine.resolve_screen_triggers(screen_2) == [] def test_append_action_to_screen_trigger_attaches_a_leaf_at_the_end_of_the_chain( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) ok = game_engine.append_action_to_screen_trigger( slug, screen_id, "d_1", {"type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1"}, ) assert ok is True screen = game_engine.get_screen(slug, screen_id) rules = game_engine.resolve_screen_triggers(screen) assert rules[0]["action"]["then"]["type"] == "variable" def test_append_action_to_screen_trigger_returns_false_for_an_unknown_screen( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, _ = _create_jeu2d_game(client, tmp_game_slug_cleanup) assert game_engine.append_action_to_screen_trigger(slug, 999999, "d_1", {"type": "dialogue"}) is False def test_full_game_payload_exposes_screen_triggers( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers(slug, screen_id, [{"trigger": "affichage", "action": {"type": "dialogue"}}]) payload = game_engine.full_game_payload(slug) scene_screen = next(s for s in payload["screens"] if s["id"] == screen_id) assert scene_screen["screen_triggers"] == [{"trigger": "affichage", "action": {"type": "dialogue"}}] def test_screen_triggers_get_route_returns_the_saved_rules( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers(slug, screen_id, [{"trigger": "affichage", "action": {"type": "dialogue"}}]) resp = client.get(f"/game/{slug}/screens/{screen_id}/triggers") assert resp.status_code == 200 assert resp.get_json() == [{"trigger": "affichage", "action": {"type": "dialogue"}}] def test_screen_triggers_post_route_appends_a_rule( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) resp = client.post( f"/game/{slug}/screens/{screen_id}/triggers", json={"rule": {"trigger": "affichage", "action": {"type": "dialogue"}}}, ) assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) assert game_engine.resolve_screen_triggers(screen) == [{"trigger": "affichage", "action": {"type": "dialogue"}}] def test_screen_trigger_delete_one_route_removes_only_the_targeted_rule( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ {"trigger": "affichage", "action": {"type": "dialogue"}}, {"trigger": "affichage", "action": {"type": "son", "asset_url": "/assets/1/bip.mp3"}}, ], ) resp = client.delete(f"/game/{slug}/screens/{screen_id}/triggers/0") assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) rules = game_engine.resolve_screen_triggers(screen) assert len(rules) == 1 assert rules[0]["action"]["type"] == "son" def test_screen_trigger_delete_one_route_404s_for_an_out_of_range_index( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) resp = client.delete(f"/game/{slug}/screens/{screen_id}/triggers/0") assert resp.status_code == 404 def test_screen_triggers_delete_route_clears_all_rules( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers(slug, screen_id, [{"trigger": "affichage", "action": {"type": "dialogue"}}]) resp = client.delete(f"/game/{slug}/screens/{screen_id}/triggers") assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) assert game_engine.resolve_screen_triggers(screen) == [] def test_screen_triggers_get_route_backfills_and_persists_a_missing_leaf_id( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": { "type": "variable", "target_variable": "score", "data_operation": "incrementer", "data_value": "1", }, } ], ) resp1 = client.get(f"/game/{slug}/screens/{screen_id}/triggers") leaf_id = resp1.get_json()[0]["action"]["id"] assert leaf_id resp2 = client.get(f"/game/{slug}/screens/{screen_id}/triggers") assert resp2.get_json()[0]["action"]["id"] == leaf_id # stable append_resp = client.post( f"/game/{slug}/screens/{screen_id}/triggers/append-action", json={"after_id": leaf_id, "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}}, ) assert append_resp.status_code == 200 def test_screen_triggers_get_route_404s_for_an_unknown_screen( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, _ = _create_jeu2d_game(client, tmp_game_slug_cleanup) resp = client.get(f"/game/{slug}/screens/999999/triggers") assert resp.status_code == 404 def test_screen_trigger_dialogue_update_route_edits_the_matching_dialogue( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) resp = client.post( f"/game/{slug}/screens/{screen_id}/triggers/dialogue", json={"dialogue_id": "d_1", "lines": [{"speaker": "narrateur", "text": "Bienvenue !"}]}, ) assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) rules = game_engine.resolve_screen_triggers(screen) assert rules[0]["action"]["dialogue"]["lines"] == [ {"type": "dialogue", "speaker": "narrateur", "text": "Bienvenue !"} ] def test_screen_trigger_remove_action_route_removes_the_whole_rule_when_it_is_the_only_leaf( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) resp = client.delete(f"/game/{slug}/screens/{screen_id}/triggers/action/d_1") assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) assert game_engine.resolve_screen_triggers(screen) == [] def test_screen_trigger_remove_action_route_404s_for_an_unknown_leaf_id( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) resp = client.delete(f"/game/{slug}/screens/{screen_id}/triggers/action/d_absent") assert resp.status_code == 404 def test_screen_trigger_move_action_route_swaps_a_leaf_up_in_its_chain( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": { "type": "attendre", "id": "a_1", "data_value": "2", "then": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, }, } ], ) resp = client.post(f"/game/{slug}/screens/{screen_id}/triggers/action/d_1/move", json={"direction": "up"}) assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) action = game_engine.resolve_screen_triggers(screen)[0]["action"] assert action["type"] == "dialogue" assert action["then"]["type"] == "attendre" def test_screen_trigger_move_action_route_400s_for_an_invalid_direction( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) resp = client.post(f"/game/{slug}/screens/{screen_id}/triggers/action/d_1/move", json={"direction": "sideways"}) assert resp.status_code == 400 def test_screen_trigger_append_action_route_attaches_a_leaf( client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str] ) -> None: slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) game_engine.set_screen_triggers( slug, screen_id, [ { "trigger": "affichage", "action": {"type": "dialogue", "dialogue": {"id": "d_1", "lines": []}}, } ], ) resp = client.post( f"/game/{slug}/screens/{screen_id}/triggers/append-action", json={"after_id": "d_1", "action": {"type": "son", "asset_url": "/assets/1/bip.mp3"}}, ) assert resp.status_code == 200 screen = game_engine.get_screen(slug, screen_id) rules = game_engine.resolve_screen_triggers(screen) assert rules[0]["action"]["then"] == {"type": "son", "asset_url": "/assets/1/bip.mp3"}