Deux bugs remontés en test manuel sur l'éditeur de scène (Phase A,
commit f0070fa) :
1. Décalage visuel à l'ajout d'un objet de scène (personnage/décor) :
render_scene_object.py positionne son <img> en absolu (left/top/
width/height en px), pensé pour être un enfant DIRECT de
.playScreen.playScene en mode jouable. Dans scene_edit.html, la même
balise est nichée dans .canvasElementInner, lui-même déjà positionné
par .canvasElement — l'image se repositionnait donc EN PLUS depuis
#canvas (son ancêtre positionné le plus proche), en double du
décalage déjà appliqué par le conteneur. Corrigé par une règle CSS
scoped à l'éditeur (.canvasElementInner > img) qui neutralise le
positionnement propre de l'image et la fait simplement remplir son
conteneur.
2. "FOREIGN KEY constraint failed" à l'ajout d'un clip de Timeline sur
un objet de scène : _animation_clips.element_id portait une VRAIE
contrainte FK vers _screen_elements depuis la création de la table.
animation-timeline.js est réutilisé TEL QUEL entre les deux éditeurs
(voir le plan "Fondations d'une plateforme multi-éditeurs") et
n'opère aucune distinction — pour un jeu jeu_2d, element_id désigne
en réalité un id de _scene_objects, absent de _screen_elements, d'où
l'échec de l'INSERT sous PRAGMA foreign_keys=ON. ensure_animation_schema
reconstruit maintenant la table (une fois, migration automatique à la
volée comme le reste du schéma) sans cette contrainte FK — même
patron que trigger_element_id/cond_element_a dans ensure_flow_schema.py.
Comme la suppression en cascade reposait jusqu'ici sur cette FK, un
nettoyage manuel des clips a été ajouté à la suppression d'un élément
(delete_element.py) et d'un objet de scène (delete_scene_object.py).
Nouveaux tests (tests/test_scene_edit_view.py, +4 cas) : ajout d'un
clip de Timeline sur un objet de scène via la route, nettoyage des
clips à la suppression d'un objet de scène et d'un élément DOM. Suite
complète : 312 tests passent (aucune régression).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
173 lines
8.0 KiB
Python
173 lines
8.0 KiB
Python
"""Vue de l'éditeur de scène 2D (routes/scenes/scene_edit_view.py) —
|
|
appelée par routes/screens/screen_edit.py selon game_type, RÉUTILISE tel
|
|
quel le moteur de logique de flow partagé (flow-editor.js/tabs-and-blocks.js/
|
|
animation-timeline.js) — voir le plan "Fondations d'une plateforme
|
|
multi-éditeurs"."""
|
|
import json
|
|
import re
|
|
|
|
import db
|
|
import screens
|
|
from conftest import screen_edit_js_bundle
|
|
|
|
|
|
def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_scene_edit"):
|
|
resp = client.post("/games/new", data={"name": name, "game_type": "jeu_2d"}, follow_redirects=False)
|
|
slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1])
|
|
screen_id = screens.create_screen(slug, "Scène 1")
|
|
return slug, screen_id
|
|
|
|
|
|
def test_document_game_still_uses_generic_editor(client, game):
|
|
"""Non-régression — critère de succès n°1 du plan : un jeu "document"
|
|
existant continue de fonctionner à l'identique."""
|
|
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
|
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert "ÉLÉMENTS DE CET ÉCRAN" in html.upper() or "Éléments de cet écran" in html
|
|
assert "scene-editor.js" not in html
|
|
|
|
|
|
def test_jeu2d_game_dispatches_to_scene_editor(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
html = client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert "Objets de cette scène" in html
|
|
assert "scene-editor.js" in html
|
|
assert "🎭 Personnages" in html
|
|
|
|
|
|
def test_scene_edit_view_renders_with_a_personnage_object_selected(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
|
|
data={"kind": "personnage", "forge_character": "zombie"},
|
|
follow_redirects=False,
|
|
)
|
|
assert resp.status_code == 302
|
|
obj_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
|
|
html = client.get(f"/game/{slug}/screens/{screen_id}/edit?selected={obj_id}").get_data(as_text=True)
|
|
assert "/static/characters/zombie/idle.png" in html
|
|
assert f'data-object-id="{obj_id}"' in html
|
|
assert "Personnage Forge" in html
|
|
|
|
|
|
def test_scene_object_geometry_route_persists_pixels(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_id = screens.add_scene_object(slug, screen_id)
|
|
resp = client.post(
|
|
f"/game/{slug}/scene-objects/{obj_id}/geometry",
|
|
data=json.dumps({"x": 111, "y": 222, "width": 48, "height": 96}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
obj = screens.get_scene_object(slug, obj_id)
|
|
assert (obj["x"], obj["y"], obj["width"], obj["height"]) == (111, 222, 48, 96)
|
|
|
|
|
|
def test_scene_object_delete_route(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_id = screens.add_scene_object(slug, screen_id)
|
|
resp = client.post(f"/game/{slug}/scene-objects/{obj_id}/delete", follow_redirects=False)
|
|
assert resp.status_code == 302
|
|
assert screens.get_scene_object(slug, obj_id) is None
|
|
|
|
|
|
def test_flow_node_targets_scene_object(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_id = screens.add_scene_object(slug, screen_id, forge_character="zombie")
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "action", "action_type": "jouer_animation_sprite",
|
|
"target_object_id": obj_id, "data_value": json.dumps({"animation": "walk", "fps": 8, "loop": True}),
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node_id = resp.get_json()["id"]
|
|
node = screens.get_flow_node(slug, node_id)
|
|
assert node["target_object_id"] == obj_id
|
|
assert node["target_element_id"] is None
|
|
|
|
|
|
def test_flow_node_collision_condition_targets_scene_objects(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_a = screens.add_scene_object(slug, screen_id)
|
|
obj_b = screens.add_scene_object(slug, screen_id, kind="decor")
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "condition", "cond_source": "collision",
|
|
"cond_element_a": obj_a, "cond_element_b": obj_b,
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node = screens.get_flow_node(slug, resp.get_json()["id"])
|
|
assert node["cond_element_a"] == obj_a
|
|
assert node["cond_element_b"] == obj_b
|
|
|
|
|
|
def test_play_payload_renders_scene_objects_for_jeu2d_game(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
screens.add_scene_object(slug, screen_id, forge_character="robot")
|
|
payload = screens.full_game_payload(slug)
|
|
scene_payload = next(s for s in payload["screens"] if s["id"] == screen_id)
|
|
assert len(scene_payload["elements"]) == 1
|
|
assert "/static/characters/robot/idle.png" in scene_payload["elements"][0]["rendered_html"]
|
|
assert any(payload["personnage_animations"].values())
|
|
|
|
|
|
def test_play_html_renders_fixed_size_scene(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
screens.add_scene_object(slug, screen_id, forge_character="zombie")
|
|
html = client.get(f"/game/{slug}/play").get_data(as_text=True)
|
|
assert "playScene" in html
|
|
assert "/static/characters/zombie/idle.png" in html
|
|
assert f"width:{db.game_meta(slug)['name'] and 960}px" in html or "width:960px" in html
|
|
|
|
|
|
def test_screen_edit_js_bundle_contains_scene_flow_helpers():
|
|
js = screen_edit_js_bundle()
|
|
assert "FLOW_TARGETS_OBJECTS" in js
|
|
assert "toggleFlowObjectPropertyValue" in js
|
|
|
|
|
|
def test_animation_clip_add_route_accepts_a_scene_object(client, tmp_game_slug_cleanup):
|
|
"""Bug remonté par l'utilisateur : _animation_clips.element_id portait
|
|
une VRAIE contrainte FK vers _screen_elements — poser un clip de
|
|
Timeline sur un objet de scène (_scene_objects, un id space distinct)
|
|
faisait échouer l'insertion avec "FOREIGN KEY constraint failed"."""
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_id = screens.add_scene_object(slug, screen_id)
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/animations/clips/add",
|
|
data=json.dumps({"element_id": obj_id, "kind": "animate_css", "animate_name": "fadeIn"}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
clip = resp.get_json()
|
|
assert clip["element_id"] == obj_id
|
|
|
|
|
|
def test_deleting_scene_object_cleans_up_its_animation_clips(client, tmp_game_slug_cleanup):
|
|
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
|
|
obj_id = screens.add_scene_object(slug, screen_id)
|
|
screens.add_animation_clip(slug, screen_id, obj_id, kind="animate_css", animate_name="fadeIn")
|
|
screens.delete_scene_object(slug, obj_id)
|
|
assert screens.list_animation_clips(slug, screen_id) == []
|
|
|
|
|
|
def test_deleting_element_cleans_up_its_animation_clips(client, game):
|
|
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
|
|
screen_id = int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
|
el_resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/elements/add",
|
|
data={"widget": "texte"},
|
|
follow_redirects=False,
|
|
)
|
|
element_id = int(re.search(r"selected=(\d+)", el_resp.headers["Location"]).group(1))
|
|
screens.add_animation_clip(game, screen_id, element_id, kind="animate_css", animate_name="fadeIn")
|
|
screens.delete_element(game, element_id)
|
|
assert screens.list_animation_clips(game, screen_id) == []
|