Files
Forge-Engine/tests/test_sprite_animation.py
T
william 50835a18e2
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 1m0s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Le document de cadrage produit cible des formateurs non techniques créant
des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de
logique en nœuds, timeline d'animation, définitions d'objets/relations,
templates réutilisables) est une complexité hors cible que l'effort
d'ingénierie récent avait déjà abandonnée au profit du jeu_2d.

- Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire
  Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only)
- Suppression en bloc des modules exclusifs au document : routes/elements,
  routes/element_types, routes/objects, routes/legacy_actions,
  screens/elements, screens/element_types, screens/widgets,
  screens/legacy_actions, le rendu render_element_html.py et son cluster,
  templates/screen_edit.html, templates/game_dashboard.html,
  flow-editor.js/tabs-and-blocks.js/animation-timeline.js
- Dashboard toujours simplifié (un seul mode possible désormais)
- Tests document-only supprimés, tests de logique partagée (flow,
  événements personnalisés, animations) retargetés sur des écrans jeu_2d
- Aucune régression jeu_2d : 299 tests passent

Carte d'onboarding retravaillée : argumentaire RH non technique (liste à
coche, badge "Compatible LMS"), taille et interaction de retournement
ajustées.
2026-09-04 20:56:32 +02:00

136 lines
5.9 KiB
Python

"""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
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, forge_character=None):
return screens.add_scene_object(slug, screen_id, forge_character=forge_character)
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_object_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_object_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_objet_scene",
target_object_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"
assert node["target_object_id"] == el_id
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
# Phase 8 — résolution à l'exécution depuis le personnage actuellement
# assigné (voir résolution du bug ci-dessous), pas des frames figées.
assert "resolveSpriteFrames" in js
assert "personnage_animations" in js
def test_swapping_forge_character_updates_already_configured_flow_action(client, game):
"""Régression : un nœud "Jouer une animation" configuré pour un
personnage devait rester bloqué sur les frames de l'ANCIEN personnage
même après avoir changé le personnage Forge de l'élément dans
l'éditeur (signalé par l'utilisateur). data_value ne stocke plus que
le NOM de l'animation — resolveSpriteFrames() la résout à l'exécution
depuis gameData.personnage_animations, reconstruit à chaque
chargement de full_game_payload() à partir du personnage ACTUEL."""
screen_id = _create_screen(client, game)
el_id = screens.add_scene_object(game, screen_id, forge_character="male-adventurer")
_add_flow_node(
client, game, screen_id, node_type="action", action_type="jouer_animation_sprite",
target_object_id=el_id, data_value=json.dumps({"animation": "walk", "fps": 8, "loop": True}),
)
payload = screens.full_game_payload(game)
assert payload["personnage_animations"][str(el_id)]["walk"][0].startswith("/static/characters/male-adventurer/")
client.post(
f"/game/{game}/scene-objects/{el_id}/personnage",
data=json.dumps({"source": "forge", "forge_character": "zombie"}),
content_type="application/json",
)
payload = screens.full_game_payload(game)
assert payload["personnage_animations"][str(el_id)]["walk"][0].startswith("/static/characters/zombie/")