"""Widget "Personnage" (Phase 8) — refonte visuelle du système de sprites de la Phase 7, à la demande de l'utilisateur : un vrai objet posé sur la scène (galerie visuelle, pas un élément générique piloté depuis la logique de flow), affichant sa pose "idle" dès le rendu SERVEUR. Le moteur d'exécution (runSpriteAnimation, kind="sprite" de la Timeline) reste celui de la Phase 7, inchangé — seul l'auteuring dans l'éditeur est testé ici.""" import json import re from conftest import screen_edit_js_bundle import screens 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_personnage(client, slug, screen_id, forge_character=None): data = {"widget": "personnage"} if forge_character: data["forge_character"] = forge_character resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data=data, follow_redirects=False) return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) def test_freshly_created_personnage_defaults_to_forge_character(client, game): screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id) el = screens.get_element(game, el_id) data = screens.resolve_personnage_data(el) assert data["source"] == "forge" assert data["forge_character"] in screens.SPRITE_LIBRARY def test_personnage_renders_idle_frame_server_side_without_js(client, game): """Confirme l'aperçu correct même sans JS (voir render_personnage.py) : l'élément fraîchement posé montre déjà une vraie image, jamais un cassé qu'il faudrait d'abord configurer.""" screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="zombie") html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True) assert '/static/characters/zombie/idle.png' in html assert f'data-element-id="{el_id}"' in html assert 'data-personnage="1"' in html def test_add_personnage_with_specific_forge_character(client, game): screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="zombie") el = screens.get_element(game, el_id) data = screens.resolve_personnage_data(el) assert data == {"source": "forge", "forge_character": "zombie"} def test_add_custom_sprite_personnage(client, game): screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="__custom__") el = screens.get_element(game, el_id) data = screens.resolve_personnage_data(el) assert data == {"source": "custom", "animations": {}} def test_swap_forge_character_via_ajax_route(client, game): screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="male-adventurer") resp = client.post( f"/game/{game}/elements/{el_id}/personnage", data=json.dumps({"source": "forge", "forge_character": "zombie"}), content_type="application/json", ) assert resp.status_code == 200 body = resp.get_json() assert body["animations"]["idle"] == ["/static/characters/zombie/idle.png"] el = screens.get_element(game, el_id) assert screens.resolve_personnage_data(el)["forge_character"] == "zombie" def test_import_custom_animations_via_ajax_route(client, game): screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="__custom__") custom = {"source": "custom", "animations": {"idle": ["https://x/idle.png"], "marche": ["https://x/w0.png", "https://x/w1.png"]}} resp = client.post( f"/game/{game}/elements/{el_id}/personnage", data=json.dumps(custom), content_type="application/json", ) assert resp.status_code == 200 assert resp.get_json()["animations"] == custom["animations"] el = screens.get_element(game, el_id) assert screens.resolve_personnage_animations(el) == custom["animations"] def test_resolve_personnage_idle_frame_falls_back_to_first_animation(client, game): """Un sprite personnalisé sans animation nommée "idle" (juste "marche" par ex.) doit quand même montrer QUELQUE CHOSE plutôt qu'une image cassée — retombe sur la première animation disponible.""" screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="__custom__") client.post( f"/game/{game}/elements/{el_id}/personnage", data=json.dumps({"source": "custom", "animations": {"marche": ["https://x/w0.png"]}}), content_type="application/json", ) el = screens.get_element(game, el_id) assert screens.resolve_personnage_idle_frame(el) == "https://x/w0.png" def test_screen_edit_exposes_personnage_gallery_and_scripts(client, game): screen_id = _create_screen(client, game) html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True) assert "male-adventurer" in html assert "addPersonnageElement" in html or "personnageGallery" in html js = screen_edit_js_bundle() assert "addPersonnageElement" in js