"""Personnage animé par sprites (poses/images successives) — Phase 7 du plan d'extension du moteur. Deux entrées équivalentes : une action de flow "jouer_animation_sprite" (événementiel) et un clip de la Timeline d'animation de kind "sprite" (déclaratif, voir tests/test_animations.py pour le patron des clips). Comme le reste du graphe de logique côté client, la lecture réelle de l'animation n'est pas testable sans navigateur (voir play_js_bundle() — on vérifie que la logique existe dans le JS livré et que les données persistent, pas qu'elles jouent une animation).""" import json import os import re from conftest import play_js_bundle import screens _STATIC_CHARACTERS_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "static", "characters" ) def _create_screen(client, slug, name="Accueil"): resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False) return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1)) def _add_element(client, slug, screen_id, forge_character=None): return screens.add_scene_object(slug, screen_id, forge_character=forge_character) def _add_flow_node(client, slug, screen_id, **fields): resp = client.post( f"/game/{slug}/screens/{screen_id}/flow/nodes/add", data=json.dumps(fields), content_type="application/json", ) assert resp.status_code == 200 return resp.get_json()["id"] def test_sprite_animation_action_node_persists(client, game): screen_id = _create_screen(client, game) el_id = _add_element(client, game, screen_id) data_value = json.dumps({"frames": ["/static/characters/zombie/walk0.png", "/static/characters/zombie/walk1.png"], "fps": 8, "loop": True}) node_id = _add_flow_node( client, game, screen_id, node_type="action", action_type="jouer_animation_sprite", target_object_id=el_id, data_value=data_value, ) node = screens.get_flow_node(game, node_id) assert node["action_type"] == "jouer_animation_sprite" assert node["target_object_id"] == el_id assert json.loads(node["data_value"])["fps"] == 8 def test_orientation_action_node_persists(client, game): screen_id = _create_screen(client, game) el_id = _add_element(client, game, screen_id) node_id = _add_flow_node( client, game, screen_id, node_type="action", action_type="modifier_objet_scene", target_object_id=el_id, element_property="orientation", element_value="gauche", ) node = screens.get_flow_node(game, node_id) assert node["element_property"] == "orientation" assert node["element_value"] == "gauche" assert node["target_object_id"] == el_id def test_sprite_timeline_clip_persists(client, game): screen_id = _create_screen(client, game) el_id = _add_element(client, game, screen_id) sprite_data = {"frames": ["/static/characters/male-adventurer/idle.png"], "fps": 8} resp = client.post( f"/game/{game}/screens/{screen_id}/animations/clips/add", data=json.dumps({ "element_id": el_id, "kind": "sprite", "custom_keyframes": sprite_data, "start_time": "0", "iteration_count": "infinite", }), content_type="application/json", ) assert resp.status_code == 200 clip = resp.get_json() assert clip["kind"] == "sprite" assert clip["iteration_count"] == "infinite" assert json.loads(clip["custom_keyframes"])["frames"] == ["/static/characters/male-adventurer/idle.png"] def test_sprite_library_paths_exist_on_disk(client, game): for character in screens.SPRITE_LIBRARY.values(): for frames in character["animations"].values(): assert frames, "chaque animation doit avoir au moins une frame" for url in frames: assert url.startswith("/static/characters/") relative = url[len("/static/characters/"):] assert os.path.isfile(os.path.join(_STATIC_CHARACTERS_DIR, relative)), url def test_play_page_exposes_sprite_animation_runtime(client, game): js = play_js_bundle() assert "runSpriteAnimation" in js assert "'jouer_animation_sprite'" in js assert "activeSpriteAnimations" in js assert "stopAllSpriteAnimations" in js assert "forgeFlipped" in js # Phase 8 — résolution à l'exécution depuis le personnage actuellement # assigné (voir résolution du bug ci-dessous), pas des frames figées. assert "resolveSpriteFrames" in js assert "personnage_animations" in js def test_swapping_forge_character_updates_already_configured_flow_action(client, game): """Régression : un nœud "Jouer une animation" configuré pour un personnage devait rester bloqué sur les frames de l'ANCIEN personnage même après avoir changé le personnage Forge de l'élément dans l'éditeur (signalé par l'utilisateur). data_value ne stocke plus que le NOM de l'animation — resolveSpriteFrames() la résout à l'exécution depuis gameData.personnage_animations, reconstruit à chaque chargement de full_game_payload() à partir du personnage ACTUEL.""" screen_id = _create_screen(client, game) el_id = screens.add_scene_object(game, screen_id, forge_character="male-adventurer") _add_flow_node( client, game, screen_id, node_type="action", action_type="jouer_animation_sprite", target_object_id=el_id, data_value=json.dumps({"animation": "walk", "fps": 8, "loop": True}), ) payload = screens.full_game_payload(game) assert payload["personnage_animations"][str(el_id)]["walk"][0].startswith("/static/characters/male-adventurer/") client.post( f"/game/{game}/scene-objects/{el_id}/personnage", data=json.dumps({"source": "forge", "forge_character": "zombie"}), content_type="application/json", ) payload = screens.full_game_payload(game) assert payload["personnage_animations"][str(el_id)]["walk"][0].startswith("/static/characters/zombie/")