"""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_unknown_forge_character_falls_back_to_default(client, game): """L'import de sprites personnalisés est retiré pour le moment (voir element_add.py) : une valeur qui n'est pas une clé de SPRITE_LIBRARY (ex. l'ancienne tuile "__custom__") est ignorée, l'élément garde le personnage Forge par défaut plutôt que de démarrer sans animation.""" 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": "forge", "forge_character": "male-adventurer"} 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): """La résolution _personnage_data côté serveur gère toujours la source "custom" (voir personnage_data.py) même si aucune UI ne permet plus de la créer (retiré pour le moment) — vérifie que ce chemin reste correct si réintroduit plus tard.""" screen_id = _create_screen(client, game) el_id = _add_personnage(client, game, screen_id, forge_character="male-adventurer") 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="male-adventurer") 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): """Les 6 Kenney (screens.PUBLIC_SPRITE_LIBRARY) ont chacun leur tile — les sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY), eux, sont groupés par famille dans cette même galerie (un seul tile par animal, voir screens.sprite_gallery_families et le plan "Bibliothèque de sprites animaux CraftPix"), pas testés ici un par un.""" screen_id = _create_screen(client, game) html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True) for slug in screens.PUBLIC_SPRITE_LIBRARY: assert f"/static/characters/{slug}/idle.png" in html assert "/static/characters/animals/bunny/01/idle0.png" in html assert "personnageGallery" in html # L'import de sprites personnalisés est retiré pour le moment. assert "Sprite personnalisé" not in html js = screen_edit_js_bundle() assert "addPersonnageElement" in js assert "addPersonnageAnimation" not in js def test_sprite_library_has_all_six_characters_with_all_animations(client, game): """screens.PUBLIC_SPRITE_LIBRARY (les 6 Kenney, CC0) — distinct de screens.SPRITE_LIBRARY, qui inclut aussi les sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY, réservés au rôle admin, voir le plan "Bibliothèque de sprites animaux CraftPix" et test_admin_sprite_library_loaded_from_manifest).""" assert set(screens.PUBLIC_SPRITE_LIBRARY) == { "male-adventurer", "female-adventurer", "male-person", "female-person", "robot", "zombie", } for character in screens.PUBLIC_SPRITE_LIBRARY.values(): names = set(character["animations"]) assert {"idle", "walk", "run", "jump", "attack"} <= names assert len(character["animations"]["walk"]) == 8