"""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, screen_edit_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, widget="image"): resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False) return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) 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_element_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_element_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_element", target_element_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" 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 def test_screen_edit_exposes_sprite_animation_editors(client, game): screen_id = _create_screen(client, game) html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True) assert "nf-action-sprite-fields" in html assert "nf-action-sprite-target-element" in html assert "anim-clip-sprite-fields" in html js = screen_edit_js_bundle() assert "jouer_animation_sprite" in js # Phase 8 — refonte visuelle : le sélecteur d'animation est un vrai # sélecteur à miniatures scopé au personnage ciblé, plus un menu # déroulant "Personnage Forge" global (voir test_personnage_widget.py). assert "onSpriteTargetElementChange" in js assert "populateAnimClipSpriteGrid" in js