Files
Forge-Engine/tests/game/test_scene_objects.py
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

210 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Objets de scène (_scene_objects) — fondations de l'éditeur de scène 2D
(écrans "jeu_2d", voir game_engine/screens_repo/ensure_schema.py). Positions
en PIXELS fixes (pas des %, voir ensure_scene_schema.py). kind="personnage"
réutilise TELLE QUELLE la structure _personnage_data et les fonctions
resolve_personnage_* déjà écrites pour le widget "personnage" de
l'éditeur document (Phase 8) — même bibliothèque Forge, même moteur
d'animation côté client, juste une autre table de stockage."""
from collections.abc import Callable
from typing import Any
import db
import game_engine
from tests.conftest import not_none
def _create_jeu2d_game(tmp_game_slug_cleanup: Callable[[str], str], name: str = "pytest_scene_test") -> Any:
slug = tmp_game_slug_cleanup(db.create_game(name))
screen_id = game_engine.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id
def test_scene_object_defaults_to_forge_personnage(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["kind"] == "personnage"
assert obj["attributes"]["_personnage_data"]
data = game_engine.resolve_personnage_data(obj)
assert data["source"] == "forge"
assert data["forge_character"] in game_engine.SPRITE_LIBRARY
def test_add_scene_object_with_specific_forge_character(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="personnage", forge_character="zombie")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_personnage_data(obj)["forge_character"] == "zombie"
def test_add_decor_scene_object(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="decor")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["kind"] == "decor"
def test_add_decor_scene_object_with_an_image_defaults_to_200x200_with_a_150x150_collision_box(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
"""Demande explicite : un objet/décor posé depuis "Mes assets" doit
toujours démarrer à 200×200 (jamais la taille réelle, souvent bien
plus grande, ni le défaut 128×128 du schéma), avec une boîte de
collision plus petite (150×150, centrée) pour rester proche de la
silhouette réelle."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="decor", image_url="/assets/1/photo.png")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert (obj["width"], obj["height"]) == (200, 200)
collision = game_engine.resolve_collision_settings(obj)
assert (collision["width"], collision["height"]) == (150, 150)
def test_add_decor_scene_object_with_an_image_ignores_real_image_dimensions(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
"""Contrairement à "fond" (voir add_scene_object.py), un "decor" ne
doit JAMAIS être posé à la taille réelle de l'image importée — même
quand elle est connue (image_width/image_height fournis)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(
slug,
screen_id,
kind="decor",
image_url="/assets/1/photo.png",
image_width=5504,
image_height=3072,
)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert (obj["width"], obj["height"]) == (200, 200)
def test_scene_object_geometry_is_pixels_not_percent(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id)
game_engine.update_scene_object_geometry(slug, obj_id, x=320, y=200, width=128, height=128)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert (obj["x"], obj["y"], obj["width"], obj["height"]) == (320, 200, 128, 128)
def test_list_scene_objects_ordered_by_z_index(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
first = game_engine.add_scene_object(slug, screen_id)
second = game_engine.add_scene_object(slug, screen_id)
objs = game_engine.list_scene_objects(slug, screen_id)
assert [o["id"] for o in objs] == [first, second]
def test_swap_scene_object_forge_character(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, forge_character="male-adventurer")
game_engine.set_scene_object_personnage_data(slug, obj_id, {"source": "forge", "forge_character": "robot"})
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_personnage_data(obj)["forge_character"] == "robot"
def test_render_scene_object_shows_idle_frame_pixel_positioned(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, forge_character="zombie")
game_engine.update_scene_object_geometry(slug, obj_id, x=50, y=60, width=64, height=64)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
html = game_engine.render_scene_object(obj)
assert "/static/game/characters/zombie/idle.png" in html
assert "left:50" in html and "top:60" in html
assert f'data-object-id="{obj_id}"' in html
assert 'data-personnage="1"' in html
def test_render_scene_object_alt_uses_the_authored_name_rgaa(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
"""RGAA/WCAG 1.1.1 : le nom donné par l'auteur (panneau "ℹ️
Informations") sert d'alternative textuelle — jamais aucun attribut
alt du tout n'était généré avant."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, forge_character="zombie")
game_engine.set_scene_object_name(slug, obj_id, "Zombie du hangar")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
html = game_engine.render_scene_object(obj)
assert 'alt="Zombie du hangar"' in html
def test_render_scene_object_alt_is_empty_when_unnamed_rgaa(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
"""alt="" (décoratif) plutôt qu'aucun attribut du tout quand l'auteur
n'a pas nommé l'objet."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, forge_character="zombie")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
html = game_engine.render_scene_object(obj)
assert 'alt=""' in html
def test_render_scene_object_fond_alt_is_always_empty_rgaa(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
"""Un "fond" est toujours décoratif — alt="" même s'il est nommé."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="fond", background_slug="foret")
game_engine.set_scene_object_name(slug, obj_id, "Grand décor de fond")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
html = game_engine.render_scene_object(obj)
assert 'alt=""' in html
def test_add_scene_object_with_explicit_position_uses_it_instead_of_the_default(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
"""Vrai glisser-déposer depuis la galerie (demande explicite :
"n'importe où sur la scène") — x/y explicites remplacent le défaut
100/100 posé par le schéma (voir ensure_scene_schema.py)."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="decor", x=250, y=340)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["x"] == 250
assert obj["y"] == 340
def test_add_scene_object_without_position_still_defaults_to_100_100(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="decor")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["x"] == 100
assert obj["y"] == 100
def test_add_scene_object_fond_with_explicit_position_overrides_the_top_left_default(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
"""Un "fond" reste au coin haut-gauche (0,0) SANS position explicite
(comportement inchangé), mais un glisser-déposer précis doit pouvoir
le positionner ailleurs si le créateur le demande explicitement."""
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="fond", background_slug="foret", x=50, y=60)
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["x"] == 50
assert obj["y"] == 60
def test_add_scene_object_fond_without_position_still_defaults_to_top_left(
tmp_game_slug_cleanup: Callable[[str], str],
) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id, kind="fond", background_slug="foret")
obj = not_none(game_engine.get_scene_object(slug, obj_id))
assert obj["x"] == 0
assert obj["y"] == 0
def test_delete_scene_object_cleans_up_referencing_flow_nodes(tmp_game_slug_cleanup: Callable[[str], str]) -> None:
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
obj_id = game_engine.add_scene_object(slug, screen_id)
node_id = game_engine.add_flow_node(
slug,
screen_id,
node_type="action",
action_type="jouer_animation_sprite",
target_object_id=obj_id,
data_value='{"animation":"idle","fps":8,"loop":true}',
)
game_engine.delete_scene_object(slug, obj_id)
assert game_engine.get_flow_node(slug, node_id) is None