Files
Forge-Engine/tests/test_personnage_widget.py
T
williamandClaude Sonnet 5 5ead091c75
Build and deploy / test-python (push) Successful in 1m50s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Personnages : toutes les animations, tous les personnages ; retrait de l'import de sprites personnalisés
À la demande de l'utilisateur (validation de la refonte visuelle du
widget Personnage), deux ajustements :

- Bibliothèque de sprites Forge étendue aux 6 personnages du pack Kenney
  "Toon Characters" (aventurier/aventurière, personnage homme/femme,
  robot, zombie), avec TOUTES leurs poses (45 par personnage) plutôt que
  3 — regroupées en 31 animations nommées par personnage (poses
  numérotées type walk0..walk7 fusionnées en un seul cycle "walk", les
  autres restant des poses figées à une image). ~1,2 Mo au total,
  toujours un sous-ensemble curé du pack source (assets/characters/, non
  versionné) — HD/Parts/Tilesheet/Vector toujours exclus.
- Import de sprites personnalisés retiré pour le moment : plus de tuile
  "➕ Sprite personnalisé" dans la galerie d'ajout, plus de section
  d'import dans les propriétés d'un personnage — seuls les personnages
  Forge restent proposés. La résolution serveur de _personnage_data
  garde son support générique de la source "custom" (aucune migration
  requise si cette possibilité revient plus tard), mais plus aucune UI
  ne permet de la créer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 07:38:22 +02:00

141 lines
6.5 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):
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.SPRITE_LIBRARY:
assert f"/static/characters/{slug}/idle.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):
assert set(screens.SPRITE_LIBRARY) == {
"male-adventurer", "female-adventurer", "male-person",
"female-person", "robot", "zombie",
}
for character in screens.SPRITE_LIBRARY.values():
names = set(character["animations"])
assert {"idle", "walk", "run", "jump", "attack"} <= names
assert len(character["animations"]["walk"]) == 8