Files
Forge-Engine/tests/test_personnage_widget.py
T
williamandClaude Sonnet 5 449c36fd5d
Build and deploy / test-python (push) Failing after 12s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Bibliothèque de sprites animaux CraftPix (1/3) : catalogue, galerie, accès admin
Premier commit d'une fonctionnalité découpée en plusieurs lots (voir le
plan "Bibliothèque de sprites animaux CraftPix") : intègre 14 familles
d'animaux (15 variantes de couleur chacune) comme personnages Forge
sélectionnables, à côté des 6 Kenney existants — réservé au rôle admin,
licence CraftPix oblige (interdiction contractuelle de rendre ces sprites
utilisables par un compte "user" via l'application).

- screens/labels/animal_sprite_library.py (nouveau) : charge un manifest
  JSON généré une fois (voir scripts/generate_animal_sprite_manifest.py,
  commit suivant) et construit ADMIN_SPRITE_LIBRARY, dans le même format
  que l'existant PUBLIC_SPRITE_LIBRARY (screens/labels/sprite_library.py,
  ex-SPRITE_LIBRARY, renommé pour distinguer les deux). screens.SPRITE_LIBRARY
  reste le catalogue FUSIONNÉ (utilisé par resolve_personnage_animations
  pour la résolution runtime, sans filtrage par rôle — voir le constat
  d'exploration : le payload de jeu et /jouer/<slug> ne vérifient déjà
  aucun rôle nulle part).
- screens/labels/sprite_gallery.py (nouveau) : sprite_gallery_families()
  groupe la galerie par famille — un animal n'apparaît qu'une fois (sa
  variante "de base"), ses 15 couleurs se choisissent depuis le panneau
  de propriétés (render_variant_gallery, templates/screen_edit.html),
  répondant à la suggestion de l'utilisateur plutôt que d'encombrer la
  galerie d'ajout de 210 tuiles quasi identiques.
- routes/screens/screen_edit.py, routes/scenes/scene_edit_view.py :
  la galerie passée au template est filtrée par rôle
  (PUBLIC_SPRITE_LIBRARY pour un compte "user", SPRITE_LIBRARY complet
  pour un admin) — même idiome que core/auth_guard.py.
- core/sprite_gate.py (nouveau) + 4 routes d'écriture (element_add,
  element_set_personnage_data, scene_object_add, scene_object_personnage_data) :
  ferme la brèche d'un POST direct qui contournerait la galerie filtrée
  (403 si un compte non-admin tente d'assigner un personnage animal).
- tests/conftest.py : nouvelles fixtures user_client/user_game (compte
  "user" non-admin avec un projet assigné) pour tester le filtrage par
  rôle de bout en bout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:49:05 +02:00

152 lines
7.2 KiB
Python

"""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 <img
src=""> 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