Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
7.2 KiB
Python
147 lines
7.2 KiB
Python
import json
|
||
|
||
import db
|
||
|
||
from .ensure_scene_schema import ensure_scene_schema
|
||
|
||
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
|
||
_KINDS = {"personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widget"}
|
||
# Widgets d'interface (voir "🖥️ Interface", screens/rendering/dialogue_box_style.py) :
|
||
# assez grands pour lire une réplique/question sans redimensionner,
|
||
# jamais confondus visuellement avec un personnage/décor (128×128 par
|
||
# défaut). "score_widget" reste petit — juste un nombre.
|
||
_DIALOGUE_BOX_DEFAULT_SIZE = (320, 150)
|
||
_QUIZ_BOX_DEFAULT_SIZE = (340, 240)
|
||
_SCORE_WIDGET_DEFAULT_SIZE = (140, 50)
|
||
# "decor" (demande explicite) : taille fixe à la pose, JAMAIS la taille
|
||
# réelle de l'image importée (contrairement à "fond", voir plus bas) —
|
||
# une photo/illustration "Mes assets" est souvent bien plus grande qu'un
|
||
# petit objet de décor, 200×200 reste un point de départ raisonnable à
|
||
# ajuster ensuite à la poignée. La boîte de collision, elle, est posée
|
||
# plus PETITE que l'objet (150×150, centrée — voir collision_settings.py)
|
||
# pour rester proche de la silhouette réelle plutôt que du cadre entier.
|
||
_DECOR_DEFAULT_SIZE = (200, 200)
|
||
_DECOR_COLLISION_SIZE = (150, 150)
|
||
|
||
|
||
def add_scene_object(
|
||
slug: str,
|
||
scene_id: int,
|
||
kind: str = "personnage",
|
||
forge_character: str | None = None,
|
||
background_slug: str | None = None,
|
||
image_url: str | None = None,
|
||
image_width: int | None = None,
|
||
image_height: int | None = None,
|
||
x: float | None = None,
|
||
y: float | None = None,
|
||
) -> int:
|
||
"""Pose un nouvel objet sur une scène (voir ensure_scene_schema.py).
|
||
kind="personnage" : attributes._personnage_data, MÊME structure/mêmes
|
||
fonctions resolve_personnage_* que le widget "personnage" de l'éditeur
|
||
document (screens/rendering/personnage_data.py) — un objet de scène
|
||
"personnage" partage donc directement la bibliothèque Forge et le
|
||
système d'animation déjà écrits. kind="decor" : une simple image fixe
|
||
(attributes.src), pas d'animation. kind="fond" : même image fixe que
|
||
"decor" (render_scene_object.py ne les distingue pas au rendu), mais
|
||
posée en dessous de tout le reste (z_index très négatif, voir
|
||
static/style.css .sceneBackground) et à la taille RÉELLE de l'image
|
||
choisie (voir screens/labels/background_library.py) par défaut — une
|
||
image plus grande que la scène devient alors le "monde" que la caméra
|
||
suit en défilant (voir static/js/play/personnage-controller.js).
|
||
|
||
image_url (kind "fond"/"decor" uniquement) : une image ARBITRAIRE
|
||
(voir "Mes assets", auth/user_assets/) plutôt qu'une clé de
|
||
BACKGROUND_LIBRARY — prioritaire sur background_slug si les deux sont
|
||
fournis. image_width/image_height (kind "fond" uniquement, voir
|
||
routes/scenes/scene_object_add.py::auth.image_dimensions) : taille
|
||
RÉELLE de l'image quand connue, pour que le "monde" puisse s'agrandir
|
||
à sa vraie taille. Un "decor" les ignore volontairement (demande
|
||
explicite) : toujours posé à _DECOR_DEFAULT_SIZE (200×200), quelle que
|
||
soit la taille réelle de l'image importée.
|
||
|
||
x/y (optionnels) : position EXACTE demandée par le créateur (voir
|
||
"vrai glisser-déposer" depuis la galerie, static/js/scenes/
|
||
scene-editor.js::onGalleryDragStart/onCanvasDrop) — remplace le
|
||
défaut habituel (100,100 posé par le schéma, voir
|
||
ensure_scene_schema.py) pour TOUT type d'objet, y compris "fond"
|
||
(qui, SANS position explicite, continue de démarrer au coin
|
||
haut-gauche du monde comme avant)."""
|
||
ensure_scene_schema(slug)
|
||
kind = kind if kind in _KINDS else "personnage"
|
||
attributes = {}
|
||
width = height = None
|
||
conn = db.connect(slug)
|
||
if kind == "personnage":
|
||
data = dict(_DEFAULT_PERSONNAGE_DATA)
|
||
if forge_character:
|
||
from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||
from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||
|
||
if forge_character in PUBLIC_SPRITE_LIBRARY or forge_character in ADMIN_SPRITE_LIBRARY:
|
||
data["forge_character"] = forge_character
|
||
attributes["_personnage_data"] = json.dumps(data)
|
||
# Rôle "pnj" explicite (demande explicite : un personnage fraîchement
|
||
# posé n'est JAMAIS le joueur par défaut, même le premier de la
|
||
# scène — voir screens/rendering/personnage_role.py, "🏷️ Rôle" dans
|
||
# le panneau de propriétés pour le passer en "Joueur" à la main).
|
||
# Un rôle non défini retomberait sur "joueur" par défaut (voir
|
||
# resolve_personnage_role) : sans cette affectation explicite, un
|
||
# personnage fraîchement posé se ferait donc passer pour le joueur
|
||
# et disparaîtrait à tort de "🧩 Collision" (voir scene_edit_view.py,
|
||
# qui exclut toujours le joueur).
|
||
attributes["_personnage_role"] = "pnj"
|
||
elif kind == "fond" and image_url:
|
||
attributes["src"] = image_url
|
||
if image_width is not None and image_height is not None:
|
||
width, height = image_width, image_height
|
||
elif kind == "decor" and image_url:
|
||
attributes["src"] = image_url
|
||
width, height = _DECOR_DEFAULT_SIZE
|
||
attributes["_collision"] = json.dumps(
|
||
{
|
||
"width": _DECOR_COLLISION_SIZE[0],
|
||
"height": _DECOR_COLLISION_SIZE[1],
|
||
}
|
||
)
|
||
elif kind == "fond" and background_slug:
|
||
from ..labels.background_library import BACKGROUND_LIBRARY
|
||
|
||
background = BACKGROUND_LIBRARY.get(background_slug)
|
||
if background:
|
||
attributes["src"] = background["url"]
|
||
width, height = background["width"], background["height"]
|
||
elif kind == "dialogue_box":
|
||
width, height = _DIALOGUE_BOX_DEFAULT_SIZE
|
||
elif kind == "quiz_box":
|
||
width, height = _QUIZ_BOX_DEFAULT_SIZE
|
||
elif kind == "score_widget":
|
||
width, height = _SCORE_WIDGET_DEFAULT_SIZE
|
||
if kind == "fond":
|
||
z_index = -1000
|
||
else:
|
||
max_z = conn.execute("SELECT MAX(z_index) AS m FROM _scene_objects WHERE scene_id = ?", (scene_id,)).fetchone()[
|
||
"m"
|
||
]
|
||
z_index = (max_z or 0) + 1
|
||
cols = ["scene_id", "kind", "attributes", "z_index"]
|
||
values = [scene_id, kind, json.dumps(attributes), z_index]
|
||
if width is not None:
|
||
cols += ["width", "height"]
|
||
values += [width, height]
|
||
if x is None and y is None and kind == "fond":
|
||
# Comportement inchangé pour un fond posé SANS position explicite
|
||
# (galerie Forge/Mes assets par simple clic) : jamais le défaut
|
||
# 100/100 (voir ensure_scene_schema.py), toujours le coin
|
||
# haut-gauche de la scène/du monde.
|
||
x, y = 0, 0
|
||
if x is not None and y is not None:
|
||
cols += ["x", "y"]
|
||
values += [x, y]
|
||
placeholders = ", ".join(["?"] * len(values))
|
||
conn.execute(f"INSERT INTO _scene_objects ({', '.join(cols)}) VALUES ({placeholders})", values) # nosec B608 # noqa: S608 - cols construit uniquement a partir de litteraux Python, jamais d'une cle arbitraire ; valeurs parametrees (?)
|
||
object_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||
conn.commit()
|
||
conn.close()
|
||
return object_id
|