"""Éditeur de collision (onglet "🧩 Collision", voir screens/rendering/collision_rules.py) : une règle "déclencheur (collision/ périmètre) -> action (quête/attaque/événement/interagir)" par carte objet — toujours les AUTRES objets qui réagissent à la présence du joueur, jamais l'inverse. static/js/scenes/collision-rules-editor.js construit l'assistant "+ Action" ; ce fichier teste la couche serveur (défauts, validation, exposition dans full_game_payload).""" import re import db import screens def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_collision_rules"): resp = client.post("/games/new", data={"name": name}, follow_redirects=False) slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1]) screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d") return slug, screen_id def _add_personnage(client, slug, screen_id, role=None): resp = client.post(f"/game/{slug}/screens/{screen_id}/scene-objects/add", data={"kind": "personnage"}, follow_redirects=False) assert resp.status_code == 302, resp.data obj_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) if role: client.post(f"/game/{slug}/scene-objects/{obj_id}/role", data=f'{{"role": "{role}"}}', content_type="application/json") return obj_id def _add_custom_event(client, slug, name="NEED_HELP"): resp = client.post(f"/game/{slug}/events/new", data={"name": name}, follow_redirects=False) assert resp.status_code == 302, resp.data events = screens.list_custom_events(slug) if hasattr(screens, "list_custom_events") else __import__("db").list_custom_events(slug) return next(e["id"] for e in events if e["name"] == name) def test_freshly_placed_object_has_no_collision_rules(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") obj = screens.get_scene_object(slug, enemy_id) assert screens.resolve_collision_rules(obj) == [] def test_rules_route_persists_a_collision_trigger_attack_action(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") resp = client.post( f"/game/{slug}/scene-objects/{enemy_id}/collision-rules", data='{"rules": [{"trigger": "collision", "action": {"type": "attaque"}}]}', content_type="application/json", ) assert resp.status_code == 200 assert resp.get_json()["rules"] == [{"trigger": "collision", "action": {"type": "attaque"}}] obj = screens.get_scene_object(slug, enemy_id) assert screens.resolve_collision_rules(obj) == [{"trigger": "collision", "action": {"type": "attaque"}}] def test_rules_route_persists_a_perimeter_trigger_with_custom_radius(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") resp = client.post( f"/game/{slug}/scene-objects/{enemy_id}/collision-rules", data='{"rules": [{"trigger": "perimetre", "perimetre_px": 40, "action": {"type": "attaque"}}]}', content_type="application/json", ) rules = resp.get_json()["rules"] assert rules == [{"trigger": "perimetre", "perimetre_px": 40.0, "action": {"type": "attaque"}}] def test_rules_route_persists_a_quest_action_referencing_a_quest_id(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) pnj_id = _add_personnage(client, slug, screen_id, role="pnj") quest = db.create_quest(slug, "Trouver la clé") resp = client.post( f"/game/{slug}/scene-objects/{pnj_id}/collision-rules", data='{"rules": [{"trigger": "collision", "action": {"type": "quete", "quete_id": ' + str(quest["id"]) + '}}]}', content_type="application/json", ) assert resp.get_json()["rules"] == [{"trigger": "collision", "action": {"type": "quete", "quete_id": quest["id"]}}] def test_rules_route_persists_an_interagir_action_with_one_level_of_nesting(client, tmp_game_slug_cleanup): """"interagir" affiche "Appuie sur [touche]" puis exécute une SOUS-action à l'appui — un seul niveau d'imbrication autorisé (voir screens/rendering/collision_rules.py::_MAX_ACTION_DEPTH).""" slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) pnj_id = _add_personnage(client, slug, screen_id, role="pnj") resp = client.post( f"/game/{slug}/scene-objects/{pnj_id}/collision-rules", data='{"rules": [{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": 1}}}]}', content_type="application/json", ) assert resp.get_json()["rules"] == [ {"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": 1}}}, ] def test_rules_route_rejects_interagir_nested_inside_interagir(client, tmp_game_slug_cleanup): """Une imbrication trop profonde (interagir -> interagir -> ...) est ignorée plutôt que d'accepter une structure sans fin postée à la main.""" slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) pnj_id = _add_personnage(client, slug, screen_id, role="pnj") resp = client.post( f"/game/{slug}/scene-objects/{pnj_id}/collision-rules", data='{"rules": [{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "interagir", "sub_action": {"type": "attaque"}}}}]}', content_type="application/json", ) assert resp.get_json()["rules"] == [] # la règle entière est rejetée def test_rules_route_drops_malformed_rules(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") resp = client.post( f"/game/{slug}/scene-objects/{enemy_id}/collision-rules", data='{"rules": [{"trigger": "diagonale", "action": {"type": "attaque"}}, {"trigger": "collision", "action": {"type": "sorcellerie"}}, "pas un objet"]}', content_type="application/json", ) assert resp.get_json()["rules"] == [] def test_rules_route_404s_for_missing_object(client, game): resp = client.post(f"/game/{game}/scene-objects/999999/collision-rules", data='{"rules": []}', content_type="application/json") assert resp.status_code == 404 def test_full_game_payload_excludes_the_player_and_fond_from_collision_rule_objects(client, tmp_game_slug_cleanup): """Toujours les AUTRES objets qui réagissent à la présence du joueur, jamais l'inverse — le joueur lui-même et un fond n'ont pas de champ "collision_rules" dans le payload.""" slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) player_id = _add_personnage(client, slug, screen_id) # rôle par défaut : joueur enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") payload = screens.full_game_payload(slug) scene_screen = next(s for s in payload["screens"] if s["id"] == screen_id) player_payload = next(o for o in scene_screen["elements"] if o["id"] == player_id) enemy_payload = next(o for o in scene_screen["elements"] if o["id"] == enemy_id) assert "collision_rules" not in player_payload assert enemy_payload["collision_rules"] == [] def test_scene_edit_view_lists_eligible_objects_only(client, tmp_game_slug_cleanup): slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup) player_id = _add_personnage(client, slug, screen_id) # rôle par défaut : joueur, exclu enemy_id = _add_personnage(client, slug, screen_id, role="ennemie") html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True) idx = html.find("var COLLISION_RULE_OBJECTS") end = html.find(";", idx) snippet = html[idx:end] assert f'"id": {enemy_id}' in snippet assert f'"id": {player_id}' not in snippet